solarwindpy 0.0.1.dev0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of solarwindpy might be problematic. Click here for more details.
- plans/.velocity/metrics.json +96 -0
- plans/0-overview-template.md +268 -0
- plans/N-phase-template.md +106 -0
- plans/PLAN_AUDIT_SUMMARY.md +173 -0
- plans/TEMPLATE-USAGE-GUIDE.md +198 -0
- plans/__init__.py +1 -0
- plans/abandoned/compaction-agent-system/0-Overview.md +123 -0
- plans/abandoned/compaction-agent-system/agents-index-update-plan.md +109 -0
- plans/abandoned/compaction-agent-system/compacted_state.md +85 -0
- plans/abandoned/compaction-agent-system/implementation-plan.md +107 -0
- plans/abandoned/compaction-agent-system/system-validation-report.md +159 -0
- plans/abandoned/compaction-agent-system/usage-guide.md +210 -0
- plans/abandoned/hook-system-enhancement/0-Overview.md +214 -0
- plans/abandoned/hook-system-enhancement/1-Phase1-Core-Infrastructure.md +313 -0
- plans/abandoned/hook-system-enhancement/2-Phase2-Intelligent-Testing.md +385 -0
- plans/abandoned/hook-system-enhancement/3-Phase3-Physics-Validation.md +444 -0
- plans/abandoned/hook-system-enhancement/4-Phase4-Performance-Monitoring.md +458 -0
- plans/abandoned/hook-system-enhancement/5-Phase5-Developer-Experience.md +532 -0
- plans/abandoned/hook-system-enhancement/6-Implementation-Timeline.md +274 -0
- plans/abandoned/hook-system-enhancement/7-Risk-Management.md +376 -0
- plans/abandoned/hook-system-enhancement/8-Testing-Strategy.md +579 -0
- plans/abandoned/readthedocs-automation/0-Overview.md +247 -0
- plans/abandoned/readthedocs-automation/1-Emergency-Documentation-Fixes.md +270 -0
- plans/abandoned/readthedocs-automation/2-Template-System-Enhancement.md +811 -0
- plans/abandoned/readthedocs-automation/3-Quality-Audit-ReadTheDocs-Integration.md +844 -0
- plans/abandoned/readthedocs-automation/4-Plan-Consolidation-Cleanup.md +632 -0
- plans/abandoned/readthedocs-automation/9-Closeout.md +207 -0
- plans/abandoned/readthedocs-automation/ABANDONMENT_REASON.md +72 -0
- plans/cicd-architecture-redesign/0-Overview.md +193 -0
- plans/cicd-architecture-redesign/1-Workflow-Creation.md +103 -0
- plans/cicd-architecture-redesign/2-Version-Detection.md +123 -0
- plans/cicd-architecture-redesign/3-Deployment-Gates.md +169 -0
- plans/cicd-architecture-redesign/4-RC-Testing.md +194 -0
- plans/cicd-architecture-redesign/5-TestPyPI-Validation.md +264 -0
- plans/cicd-architecture-redesign/6-Production-Release.md +263 -0
- plans/cicd-architecture-redesign/7-Cleanup.md +243 -0
- plans/cicd-architecture-redesign/8-Documentation.md +285 -0
- plans/cicd-architecture-redesign/Closeout.md +225 -0
- plans/closeout-template.md +259 -0
- plans/completed/circular-import-audit/0-Overview.md +152 -0
- plans/completed/circular-import-audit/1-Static-Dependency-Analysis.md +62 -0
- plans/completed/circular-import-audit/2-Dynamic-Import-Testing.md +56 -0
- plans/completed/circular-import-audit/3-Performance-Impact-Assessment.md +56 -0
- plans/completed/circular-import-audit/4-Issue-Remediation.md +78 -0
- plans/completed/circular-import-audit/5-Preventive-Infrastructure.md +89 -0
- plans/completed/claude-settings-ecosystem-alignment/0-Overview.md +162 -0
- plans/completed/claude-settings-ecosystem-alignment/1-Security-Foundation.md +148 -0
- plans/completed/claude-settings-ecosystem-alignment/2-Hook-Integration.md +158 -0
- plans/completed/claude-settings-ecosystem-alignment/3-Agent-System-Integration.md +177 -0
- plans/completed/claude-settings-ecosystem-alignment/4-Enhanced-Workflow-Automation.md +159 -0
- plans/completed/claude-settings-ecosystem-alignment/5-Validation-Monitoring.md +181 -0
- plans/completed/claude-settings-ecosystem-alignment/compacted_session_state.md +290 -0
- plans/completed/combined_plan_with_checklist_documentation/1-Overview-and-Goals.md +51 -0
- plans/completed/combined_plan_with_checklist_documentation/2-Toolchain-and-Hosting.md +69 -0
- plans/completed/combined_plan_with_checklist_documentation/3-Repository-Structure.md +61 -0
- plans/completed/combined_plan_with_checklist_documentation/4-Configuration-and-Standards.md +70 -0
- plans/completed/combined_plan_with_checklist_documentation/5-Documentation-Content.md +62 -0
- plans/completed/combined_plan_with_checklist_documentation/6-CI-CD-and-Validation.md +58 -0
- plans/completed/combined_plan_with_checklist_documentation/7-Maintenance.md +55 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/0-Overview.md +135 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/1-Common-fixtures.md +59 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/10-power_laws.md +56 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/2-core.py-FitFunction.md +118 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/3-gaussians.py-Gaussian-GaussianNormalized-GaussianLn.md +69 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/4-trend_fits.py-TrendFit.md +99 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/5-plots.py-FFPlot.md +98 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/6-tex_info.py-TeXinfo.md +79 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/7-Justification.md +49 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/8-exponentials.md +64 -0
- plans/completed/combined_test_plan_with_checklist_fitfunctions/9-lines.md +58 -0
- plans/completed/combined_test_plan_with_checklist_plotting/0-Overview.md +142 -0
- plans/completed/combined_test_plan_with_checklist_plotting/1-base.py.md +90 -0
- plans/completed/combined_test_plan_with_checklist_plotting/10-labels-special.py.md +102 -0
- plans/completed/combined_test_plan_with_checklist_plotting/11-labels-chemistry.py.md +212 -0
- plans/completed/combined_test_plan_with_checklist_plotting/12-labels-composition.py.md +242 -0
- plans/completed/combined_test_plan_with_checklist_plotting/13-labels-datetime.py.md +247 -0
- plans/completed/combined_test_plan_with_checklist_plotting/14-labels-elemental_abundance.py.md +274 -0
- plans/completed/combined_test_plan_with_checklist_plotting/15-visual-validation.md +256 -0
- plans/completed/combined_test_plan_with_checklist_plotting/16-integration-testing.md +266 -0
- plans/completed/combined_test_plan_with_checklist_plotting/17-performance-benchmarks.md +267 -0
- plans/completed/combined_test_plan_with_checklist_plotting/18-Fixtures-and-Utilities.md +86 -0
- plans/completed/combined_test_plan_with_checklist_plotting/2-agg_plot.py.md +90 -0
- plans/completed/combined_test_plan_with_checklist_plotting/3-histograms.py.md +201 -0
- plans/completed/combined_test_plan_with_checklist_plotting/4-scatter.py.md +167 -0
- plans/completed/combined_test_plan_with_checklist_plotting/5-spiral.py.md +216 -0
- plans/completed/combined_test_plan_with_checklist_plotting/6-orbits.py.md +108 -0
- plans/completed/combined_test_plan_with_checklist_plotting/7-tools.py.md +86 -0
- plans/completed/combined_test_plan_with_checklist_plotting/8-select_data_from_figure.py.md +97 -0
- plans/completed/combined_test_plan_with_checklist_plotting/9-labels-base.py.md +88 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/.gitkeep +0 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/0-Overview.md +170 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/1-Package-Entry-Point-__init__.py.md +121 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/2-Core-Base-Classes-base.py.md +142 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/3-Plotting-Helpers-plots.py.md +123 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/4-LISIRD-Sub-package.md +119 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/5-Extrema-Calculator.md +103 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/6-Sunspot-Number-Sub-package.md +163 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/7-Sunspot-Number-Init.py.md +217 -0
- plans/completed/combined_test_plan_with_checklist_solar_activity/compacted_state.md +52 -0
- plans/completed/compaction-agent-modernization/0-Overview.md +156 -0
- plans/completed/compaction-agent-modernization/1-Architecture-Audit-Gap-Analysis.md +132 -0
- plans/completed/compaction-agent-modernization/2-Token-Baseline-Recalibration.md +153 -0
- plans/completed/compaction-agent-modernization/3-Agent-Reference-Updates.md +184 -0
- plans/completed/compaction-agent-modernization/4-Compression-Algorithm-Modernization.md +238 -0
- plans/completed/compaction-agent-modernization/5-Workflow-Integration-Streamlining.md +252 -0
- plans/completed/compaction-agent-modernization/6-Template-Structure-Optimization.md +240 -0
- plans/completed/compaction-agent-modernization/7-Integration-Testing-Validation.md +292 -0
- plans/completed/compaction-hook-enhancement/0-Overview.md +150 -0
- plans/completed/compaction-hook-enhancement/1-Token-Estimation-Enhancement.md +179 -0
- plans/completed/compaction-hook-enhancement/2-Compression-Intelligence.md +294 -0
- plans/completed/compaction-hook-enhancement/3-Git-Integration-Metadata.md +310 -0
- plans/completed/compaction-hook-enhancement/4-Session-Continuity-Features.md +358 -0
- plans/completed/compaction-hook-enhancement/5-Testing-Strategy.md +404 -0
- plans/completed/compaction-hook-enhancement/6-Integration-Roadmap.md +319 -0
- plans/completed/compaction-hook-enhancement/compacted_state.md +142 -0
- plans/completed/docstring-audit-enhancement/0-Overview.md +274 -0
- plans/completed/docstring-audit-enhancement/1-Infrastructure-Setup-and-Validation-Tools.md +206 -0
- plans/completed/docstring-audit-enhancement/2-Core-Physics-Modules-Enhancement.md +237 -0
- plans/completed/docstring-audit-enhancement/3-Fitfunctions-Mathematical-Modules-Enhancement.md +188 -0
- plans/completed/docstring-audit-enhancement/4-Plotting-Visualization-Modules-Enhancement.md +243 -0
- plans/completed/docstring-audit-enhancement/5-Specialized-Modules-Enhancement.md +216 -0
- plans/completed/docstring-audit-enhancement/6-Validation-and-Integration.md +216 -0
- plans/completed/fitfunctions-testing-implementation/0-Overview.md +130 -0
- plans/completed/fitfunctions-testing-implementation/1-Test-Infrastructure-Setup.md +79 -0
- plans/completed/fitfunctions-testing-implementation/2-Common-Fixtures-Test-Utilities.md +104 -0
- plans/completed/fitfunctions-testing-implementation/3-Core-FitFunction-Testing.md +168 -0
- plans/completed/fitfunctions-testing-implementation/4-Specialized-Function-Classes.md +210 -0
- plans/completed/fitfunctions-testing-implementation/5-Advanced-Classes-Testing.md +214 -0
- plans/completed/fitfunctions-testing-implementation/6-Plotting-Integration-Testing.md +231 -0
- plans/completed/fitfunctions-testing-implementation/7-Extended-Coverage-BONUS.md +184 -0
- plans/completed/numpy-docstring-conversion-plan/numpy-docstring-conversion-plan.md +118 -0
- plans/completed/pr-review-remediation/0-Overview.md +138 -0
- plans/completed/pr-review-remediation/1-Critical-Safety-Improvements.md +179 -0
- plans/completed/pr-review-remediation/2-Smart-Timeouts-Validation.md +399 -0
- plans/completed/pr-review-remediation/3-Enhanced-GitHub-Integration.md +258 -0
- plans/completed/pr-review-remediation/compacted_state.md +66 -0
- plans/completed/python-310-migration/0-Overview.md +390 -0
- plans/completed/python-310-migration/1-Planning-Setup.md +164 -0
- plans/completed/python-310-migration/2-Implementation.md +256 -0
- plans/completed/python-310-migration/3-Testing-Validation.md +335 -0
- plans/completed/python-310-migration/4-Documentation-Release.md +274 -0
- plans/completed/python-310-migration/5-Closeout.md +252 -0
- plans/completed/requirements-management-consolidation/0-Overview.md +118 -0
- plans/completed/requirements-management-consolidation/1-Documentation-Validation-Environment-Setup.md +116 -0
- plans/completed/requirements-management-consolidation/2-Requirements-Consolidation.md +161 -0
- plans/completed/requirements-management-consolidation/3-Workflow-Automation-Final-Integration.md +196 -0
- plans/completed/single-ecosystem-plan-implementation/0-Overview.md +83 -0
- plans/completed/single-ecosystem-plan-implementation/1-Plan-Preservation-Session-Management.md +38 -0
- plans/completed/single-ecosystem-plan-implementation/2-File-Structure-Optimization.md +43 -0
- plans/completed/single-ecosystem-plan-implementation/3-Plan-Migration-Archive-Setup.md +82 -0
- plans/completed/single-ecosystem-plan-implementation/4-Agent-System-Transformation.md +108 -0
- plans/completed/single-ecosystem-plan-implementation/5-Template-System-Enhancement.md +131 -0
- plans/completed/single-ecosystem-plan-implementation/6-Final-Validation-Testing.md +120 -0
- plans/completed/test-directory-consolidation/0-Overview.md +51 -0
- plans/completed/test-directory-consolidation/1-Structure-Preparation.md +82 -0
- plans/completed/test-directory-consolidation/2-File-Migration.md +100 -0
- plans/completed/test-directory-consolidation/3-Import-Transformation.md +117 -0
- plans/completed/test-directory-consolidation/4-Configuration-Consolidation.md +140 -0
- plans/completed/test-directory-consolidation/5-Validation.md +152 -0
- plans/completed/test-directory-consolidation/6-Cleanup.md +156 -0
- plans/completed/test-planning-agents-architecture/0-Overview.md +79 -0
- plans/completed/test-planning-agents-architecture/1-Branch-Isolation-Testing.md +49 -0
- plans/completed/test-planning-agents-architecture/2-Cross-Branch-Coordination.md +51 -0
- plans/completed/test-planning-agents-architecture/3-Merge-Workflow-Testing.md +48 -0
- plans/deployment-semver-pypi-rtd/0-Overview.md +463 -0
- plans/deployment-semver-pypi-rtd/1-Semantic-Versioning-Foundation.md +136 -0
- plans/deployment-semver-pypi-rtd/2-PyPI-Deployment-Infrastructure.md +168 -0
- plans/deployment-semver-pypi-rtd/3-Release-Automation.md +214 -0
- plans/deployment-semver-pypi-rtd/4-Plan-Closeout.md +543 -0
- plans/deployment-semver-pypi-rtd/compacted_session_state.md +172 -0
- plans/deployment-semver-pypi-rtd/compacted_state.md +131 -0
- plans/documentation-code-audit/0-Overview.md +393 -0
- plans/documentation-code-audit/1-Discovery-Inventory.md +183 -0
- plans/documentation-code-audit/2-Execution-Environment-Setup.md +263 -0
- plans/documentation-code-audit/3-Systematic-Validation.md +322 -0
- plans/documentation-code-audit/4-Code-Example-Remediation.md +358 -0
- plans/documentation-code-audit/5-Physics-MultiIndex-Compliance.md +464 -0
- plans/documentation-code-audit/6-Doctest-Integration.md +523 -0
- plans/documentation-code-audit/7-Reporting-Documentation.md +498 -0
- plans/documentation-code-audit/8-Closeout.md +456 -0
- plans/documentation-rebuild-session/compacted_state.md +109 -0
- plans/documentation-rendering-fixes/0-Overview.md +104 -0
- plans/documentation-rendering-fixes/1-Sphinx-Build-Diagnostics-Warning-Audit.md +101 -0
- plans/documentation-rendering-fixes/2-Configuration-Infrastructure-Fixes.md +113 -0
- plans/documentation-rendering-fixes/3-Docstring-Syntax-Audit-Repair.md +131 -0
- plans/documentation-rendering-fixes/4-HTML-Page-Rendering-Verification.md +113 -0
- plans/documentation-rendering-fixes/5-Advanced-Documentation-Quality-Assurance.md +119 -0
- plans/documentation-rendering-fixes/6-Documentation-Build-Optimization-Testing.md +129 -0
- plans/documentation-rendering-fixes/compacted_state.md +132 -0
- plans/documentation-template-fix/0-Overview.md +197 -0
- plans/documentation-template-fix/1-Template-System-Analysis.md +269 -0
- plans/documentation-template-fix/2-Template-Modification.md +609 -0
- plans/documentation-template-fix/3-Build-System-Integration.md +766 -0
- plans/documentation-template-fix/4-Testing-Validation.md +1399 -0
- plans/documentation-template-fix/5-Documentation-Training.md +602 -0
- plans/documentation-workflow-fix/0-Overview.md +222 -0
- plans/documentation-workflow-fix/1-Immediate-Fixes.md +238 -0
- plans/documentation-workflow-fix/2-Configuration-Setup.md +298 -0
- plans/documentation-workflow-fix/3-Pre-commit-Integration.md +382 -0
- plans/documentation-workflow-fix/4-Workflow-Improvements.md +446 -0
- plans/documentation-workflow-fix/5-Documentation-and-Training.md +527 -0
- plans/duplicate-object-warnings-fix-plan.md +130 -0
- plans/github-issues-migration/0-Overview.md +510 -0
- plans/github-issues-migration/1-Foundation-Label-System.md +180 -0
- plans/github-issues-migration/2-Migration-Tool-Rewrite.md +235 -0
- plans/github-issues-migration/3-CLI-Integration-Automation.md +169 -0
- plans/github-issues-migration/4-Validated-Migration.md +252 -0
- plans/github-issues-migration/5-Documentation-Training.md +171 -0
- plans/github-issues-migration/6-Closeout.md +179 -0
- plans/github-workflows-repair/repair-plan.md +299 -0
- plans/issues_from_plans.py +342 -0
- plans/pr-270-doc-validation-fixes/0-Overview.md +354 -0
- plans/pr-270-doc-validation-fixes/1-Critical-PR-Fixes.md +117 -0
- plans/pr-270-doc-validation-fixes/2-Framework-Right-Sizing.md +129 -0
- plans/pr-270-doc-validation-fixes/3-Sustainable-Documentation.md +126 -0
- plans/pr-270-doc-validation-fixes/4-Closeout-Migration.md +143 -0
- plans/pr-270-doc-validation-fixes/PLAN_COMPLETED.md +149 -0
- plans/python-310-migration/0-Overview.md +390 -0
- plans/python-310-migration/1-Planning-Setup.md +164 -0
- plans/python-310-migration/2-Implementation.md +256 -0
- plans/python-310-migration/3-Testing-Validation.md +335 -0
- plans/python-310-migration/4-Documentation-Release.md +274 -0
- plans/python-310-migration/5-Closeout.md +252 -0
- plans/readthedocs-simplified/0-Overview.md +243 -0
- plans/readthedocs-simplified/1-Immediate-Fixes.md +216 -0
- plans/readthedocs-simplified/2-Template-Simplification.md +278 -0
- plans/readthedocs-simplified/3-ReadTheDocs-Setup.md +298 -0
- plans/readthedocs-simplified/4-Testing-Validation.md +328 -0
- plans/readthedocs-simplified/5-Closeout.md +231 -0
- plans/readthedocs-simplified/compacted_state.md +127 -0
- plans/session-compaction-2025-08-12/compacted_state.md +114 -0
- plans/session-compaction-2025-08-13/compacted_state.md +145 -0
- plans/session-continuity-protocol/0-Overview.md +35 -0
- plans/session-continuity-protocol/1-Core-Principles-Framework.md +40 -0
- plans/session-continuity-protocol/2-Pre-Session-Validation-System.md +79 -0
- plans/session-continuity-protocol/3-Context-Switching-Prevention.md +87 -0
- plans/session-continuity-protocol/4-Progress-Tracking-Recovery.md +100 -0
- plans/sphinx-warnings-analysis.md +222 -0
- plans/systemprompt-optimization/0-Overview.md +447 -0
- plans/systemprompt-optimization/1-Deploy-SystemPrompt.md +114 -0
- plans/systemprompt-optimization/2-Documentation-Alignment.md +198 -0
- plans/systemprompt-optimization/3-Monitoring-Infrastructure.md +396 -0
- plans/systemprompt-optimization/4-Implementation-Script.md +450 -0
- plans/systemprompt-optimization/9-Closeout.md +165 -0
- plans/systemprompt-optimization/compacted_state.md +143 -0
- plans/template-value-propositions/0-Overview.md +357 -0
- plans/template-value-propositions/1-Value-Proposition-Framework-Design.md +144 -0
- plans/template-value-propositions/2-Plan-Template-Enhancement.md +178 -0
- plans/template-value-propositions/3-Value-Generator-Hook-Implementation.md +291 -0
- plans/template-value-propositions/4-Value-Validator-Hook-Implementation.md +274 -0
- plans/template-value-propositions/5-Documentation-Agent-Updates.md +219 -0
- plans/template-value-propositions/6-Integration-Testing-Validation.md +247 -0
- plans/tests-audit/0-Overview.md +410 -0
- plans/tests-audit/1-Discovery-Inventory.md +170 -0
- plans/tests-audit/2-Physics-Validation-Audit.md +195 -0
- plans/tests-audit/3-Architecture-Compliance.md +195 -0
- plans/tests-audit/4-Numerical-Stability-Analysis.md +203 -0
- plans/tests-audit/5-Documentation-Enhancement.md +220 -0
- plans/tests-audit/6-Audit-Deliverables.md +220 -0
- plans/tests-audit/7-Closeout.md +252 -0
- plans/tests-audit/artifacts/ARCHITECTURE_COMPLIANCE_REPORT.md +315 -0
- plans/tests-audit/artifacts/ARCHITECTURE_RECOMMENDATIONS.md +943 -0
- plans/tests-audit/artifacts/COMPREHENSIVE_AUDIT_REPORT.md +356 -0
- plans/tests-audit/artifacts/CONTRIBUTING_ENHANCED_TEMPLATE.md +419 -0
- plans/tests-audit/artifacts/COVERAGE_GAP_ANALYSIS.md +152 -0
- plans/tests-audit/artifacts/DOCUMENTATION_ENHANCEMENT_REPORT.md +502 -0
- plans/tests-audit/artifacts/EXECUTIVE_AUDIT_SUMMARY.md +129 -0
- plans/tests-audit/artifacts/IMPLEMENTATION_ROADMAP.md +647 -0
- plans/tests-audit/artifacts/NUMERICAL_RECOMMENDATIONS.md +739 -0
- plans/tests-audit/artifacts/NUMERICAL_STABILITY_GUIDE_TEMPLATE.rst +451 -0
- plans/tests-audit/artifacts/NUMERICAL_STABILITY_REPORT.md +301 -0
- plans/tests-audit/artifacts/PHASE_3_SUMMARY.md +280 -0
- plans/tests-audit/artifacts/PHASE_4_SUMMARY.md +229 -0
- plans/tests-audit/artifacts/PHASE_5_SUMMARY.md +292 -0
- plans/tests-audit/artifacts/PHASE_6_CLOSEOUT.md +278 -0
- plans/tests-audit/artifacts/PHYSICS_GUIDE_TEMPLATE.rst +268 -0
- plans/tests-audit/artifacts/PHYSICS_VALIDATION_REPORT.md +235 -0
- plans/tests-audit/artifacts/TECHNICAL_DELIVERABLES_PACKAGE.md +2502 -0
- plans/tests-audit/artifacts/TEST_INVENTORY.csv +1204 -0
- plans/tests-audit/artifacts/TEST_INVENTORY.md +135 -0
- plans/tests-audit/artifacts/test_discovery_analysis.py +231 -0
- plans/tests-audit/artifacts/test_parser.py +395 -0
- solarwindpy/README.md +3 -0
- solarwindpy/Untitled.ipynb +54 -0
- solarwindpy/__init__.py +74 -0
- solarwindpy/core/__init__.py +23 -0
- solarwindpy/core/alfvenic_turbulence.py +804 -0
- solarwindpy/core/base.py +267 -0
- solarwindpy/core/ions.py +309 -0
- solarwindpy/core/plasma.py +2133 -0
- solarwindpy/core/spacecraft.py +256 -0
- solarwindpy/core/tensor.py +90 -0
- solarwindpy/core/units_constants.py +199 -0
- solarwindpy/core/vector.py +328 -0
- solarwindpy/fitfunctions/__init__.py +20 -0
- solarwindpy/fitfunctions/core.py +734 -0
- solarwindpy/fitfunctions/exponentials.py +188 -0
- solarwindpy/fitfunctions/gaussians.py +264 -0
- solarwindpy/fitfunctions/lines.py +116 -0
- solarwindpy/fitfunctions/moyal.py +71 -0
- solarwindpy/fitfunctions/plots.py +751 -0
- solarwindpy/fitfunctions/power_laws.py +209 -0
- solarwindpy/fitfunctions/tex_info.py +568 -0
- solarwindpy/fitfunctions/trend_fits.py +482 -0
- solarwindpy/instabilities/__init__.py +16 -0
- solarwindpy/instabilities/beta_ani.py +82 -0
- solarwindpy/instabilities/verscharen2016.py +631 -0
- solarwindpy/plotting/__init__.py +33 -0
- solarwindpy/plotting/agg_plot.py +489 -0
- solarwindpy/plotting/base.py +465 -0
- solarwindpy/plotting/hist1d.py +405 -0
- solarwindpy/plotting/hist2d.py +1035 -0
- solarwindpy/plotting/histograms.py +1845 -0
- solarwindpy/plotting/labels/__init__.py +104 -0
- solarwindpy/plotting/labels/base.py +686 -0
- solarwindpy/plotting/labels/chemistry.py +19 -0
- solarwindpy/plotting/labels/composition.py +100 -0
- solarwindpy/plotting/labels/datetime.py +235 -0
- solarwindpy/plotting/labels/elemental_abundance.py +73 -0
- solarwindpy/plotting/labels/special.py +794 -0
- solarwindpy/plotting/orbits.py +515 -0
- solarwindpy/plotting/scatter.py +99 -0
- solarwindpy/plotting/select_data_from_figure.py +329 -0
- solarwindpy/plotting/spiral.py +980 -0
- solarwindpy/plotting/tools.py +434 -0
- solarwindpy/scripts/__init__.py +1 -0
- solarwindpy/scripts/logs/.gitignore +1 -0
- solarwindpy/solar_activity/__init__.py +53 -0
- solarwindpy/solar_activity/base.py +605 -0
- solarwindpy/solar_activity/lisird/__init__.py +3 -0
- solarwindpy/solar_activity/lisird/extrema_calculator.py +394 -0
- solarwindpy/solar_activity/lisird/lisird.py +319 -0
- solarwindpy/solar_activity/plots.py +116 -0
- solarwindpy/solar_activity/sunspot_number/.DS_Store +0 -0
- solarwindpy/solar_activity/sunspot_number/__init__.py +3 -0
- solarwindpy/solar_activity/sunspot_number/sidc.py +556 -0
- solarwindpy/solar_activity/sunspot_number/ssn_extrema.csv +72 -0
- solarwindpy/solar_activity/sunspot_number/ssn_extrema.csv.silso +72 -0
- solarwindpy/tools/__init__.py +162 -0
- solarwindpy-0.1.1.dist-info/METADATA +181 -0
- solarwindpy-0.1.1.dist-info/RECORD +409 -0
- {solarwindpy-0.0.1.dev0.dist-info → solarwindpy-0.1.1.dist-info}/WHEEL +1 -1
- solarwindpy-0.1.1.dist-info/licenses/LICENSE.rst +32 -0
- solarwindpy-0.1.1.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/conftest.py +10 -0
- tests/core/__init__.py +1 -0
- tests/core/test_alfvenic_turbulence.py +544 -0
- tests/core/test_base.py +112 -0
- tests/core/test_base_head_tail.py +29 -0
- tests/core/test_base_mi_tuples.py +11 -0
- tests/core/test_core_verify_datetimeindex.py +32 -0
- tests/core/test_ions.py +325 -0
- tests/core/test_plasma.py +2581 -0
- tests/core/test_plasma_io.py +12 -0
- tests/core/test_quantities.py +507 -0
- tests/core/test_spacecraft.py +210 -0
- tests/core/test_units_constants.py +22 -0
- tests/data/epoch.csv +4 -0
- tests/data/plasma.csv +4 -0
- tests/data/spacecraft.csv +4 -0
- tests/fitfunctions/conftest.py +60 -0
- tests/fitfunctions/test_core.py +193 -0
- tests/fitfunctions/test_exponentials.py +342 -0
- tests/fitfunctions/test_gaussians.py +142 -0
- tests/fitfunctions/test_lines.py +349 -0
- tests/fitfunctions/test_moyal.py +258 -0
- tests/fitfunctions/test_plots.py +258 -0
- tests/fitfunctions/test_power_laws.py +365 -0
- tests/fitfunctions/test_tex_info.py +183 -0
- tests/fitfunctions/test_trend_fit_properties.py +31 -0
- tests/fitfunctions/test_trend_fits.py +244 -0
- tests/plotting/__init__.py +1 -0
- tests/plotting/labels/__init__.py +1 -0
- tests/plotting/labels/test_chemistry.py +243 -0
- tests/plotting/labels/test_composition.py +345 -0
- tests/plotting/labels/test_datetime.py +445 -0
- tests/plotting/labels/test_elemental_abundance.py +366 -0
- tests/plotting/labels/test_init.py +66 -0
- tests/plotting/labels/test_labels_base.py +347 -0
- tests/plotting/labels/test_special.py +550 -0
- tests/plotting/test_agg_plot.py +602 -0
- tests/plotting/test_base.py +752 -0
- tests/plotting/test_fixtures_utilities.py +775 -0
- tests/plotting/test_histograms.py +546 -0
- tests/plotting/test_integration.py +675 -0
- tests/plotting/test_orbits.py +435 -0
- tests/plotting/test_performance.py +708 -0
- tests/plotting/test_scatter.py +752 -0
- tests/plotting/test_select_data_from_figure.py +1209 -0
- tests/plotting/test_spiral.py +573 -0
- tests/plotting/test_tools.py +607 -0
- tests/plotting/test_visual_validation.py +465 -0
- tests/solar_activity/__init__.py +1 -0
- tests/solar_activity/lisird/__init__.py +1 -0
- tests/solar_activity/lisird/test_extrema_calculator.py +593 -0
- tests/solar_activity/lisird/test_lisird_id.py +187 -0
- tests/solar_activity/sunspot_number/__init__.py +1 -0
- tests/solar_activity/sunspot_number/test_init.py +399 -0
- tests/solar_activity/sunspot_number/test_sidc.py +465 -0
- tests/solar_activity/sunspot_number/test_sidc_id.py +223 -0
- tests/solar_activity/sunspot_number/test_sidc_loader.py +275 -0
- tests/solar_activity/sunspot_number/test_ssn_extrema.py +406 -0
- tests/solar_activity/test_base.py +656 -0
- tests/solar_activity/test_init.py +396 -0
- tests/solar_activity/test_plots.py +371 -0
- tests/test_circular_imports.py +408 -0
- tests/test_issue_titles.py +25 -0
- tests/test_statusline.py +298 -0
- solarwindpy-0.0.1.dev0.dist-info/METADATA +0 -14
- solarwindpy-0.0.1.dev0.dist-info/RECORD +0 -4
- solarwindpy-0.0.1.dev0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,2133 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""The Plasma class that contains all Ions, magnetic field, and spacecraft information.
|
|
3
|
+
|
|
4
|
+
Propoded Updates
|
|
5
|
+
^^^^^^^^^^^^^^^^
|
|
6
|
+
-It would be cute if one could call `plasma % a`, i.e. plasma mod
|
|
7
|
+
an ion and return a new plasma without that ion in it. Well, either
|
|
8
|
+
mod or subtract. Subtract and add probably make more sense. (20180129)
|
|
9
|
+
|
|
10
|
+
-See (https://drive.google.com/drive/folders/0ByIrJAE4KMTtaGhRcXkxNHhmY2M)
|
|
11
|
+
for the various methods that might be worth considering including __getattr__
|
|
12
|
+
vs __getattribute__, __hash__, __deepcopy__, __copy__, etc. (20180129)
|
|
13
|
+
|
|
14
|
+
-Convert `Plasma.__call__` to `Plasma.__getitem__` and `Plasma.__iter__` to
|
|
15
|
+
to allow iterating over ions. (20180316)
|
|
16
|
+
N.B. This could have complicated results as to how we actually access the
|
|
17
|
+
underlying data and objects stored in the DataFrame.
|
|
18
|
+
|
|
19
|
+
-Define `__format__` methods for use with `str.format`. (20180316)
|
|
20
|
+
|
|
21
|
+
-Define `Plasma.__len__` to return the number of ions in the plasma. (20180316)
|
|
22
|
+
|
|
23
|
+
-Split each class into its own file. Suggested by EM. (BLA 20180217)
|
|
24
|
+
|
|
25
|
+
-Add `Plasma.dropna(*args, **kwargs)` that passes everything to `plasma.data.dropna`
|
|
26
|
+
and then calls `self.__Plasma__set_ions()` to update the ions after drop. (20180404)
|
|
27
|
+
|
|
28
|
+
-Moved `_conform_species` to base.Base so that it is accessable for
|
|
29
|
+
alfvenic_turbulence.py. Did not move tests out of `test_plasma.py`. (20181121)
|
|
30
|
+
"""
|
|
31
|
+
import numpy as np
|
|
32
|
+
import pandas as pd
|
|
33
|
+
import itertools
|
|
34
|
+
|
|
35
|
+
# We rely on views via DataFrame.xs to reduce memory size and do not
|
|
36
|
+
# `.copy(deep=True)`, so we want to make sure that this doesn't
|
|
37
|
+
# accidentally cause a problem.
|
|
38
|
+
|
|
39
|
+
from . import base
|
|
40
|
+
from . import vector
|
|
41
|
+
from . import ions
|
|
42
|
+
from . import spacecraft
|
|
43
|
+
from . import alfvenic_turbulence as alf_turb
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Plasma(base.Base):
|
|
47
|
+
r"""Container for multi-species plasma physics data and analysis.
|
|
48
|
+
|
|
49
|
+
The Plasma class serves as the central container for solar wind plasma
|
|
50
|
+
analysis, combining ion moment data, magnetic field measurements, and
|
|
51
|
+
spacecraft trajectory information for comprehensive plasma physics calculations.
|
|
52
|
+
|
|
53
|
+
This class enables analysis of multi-species plasma including protons,
|
|
54
|
+
alpha particles, and heavier ions. It provides convenient access to ion
|
|
55
|
+
species through attribute shortcuts and supports advanced plasma physics
|
|
56
|
+
calculations such as plasma beta, Coulomb collision frequencies, and
|
|
57
|
+
thermal parameters.
|
|
58
|
+
|
|
59
|
+
Attribute access is first attempted on the underlying :py:attr:`ions` table
|
|
60
|
+
before falling back to ``super().__getattr__``. This allows convenient
|
|
61
|
+
shorthand such as ``plasma.a`` to access the alpha particle :class:`Ion`
|
|
62
|
+
and ``plasma.p1`` for protons.
|
|
63
|
+
|
|
64
|
+
Attributes
|
|
65
|
+
----------
|
|
66
|
+
data : pandas.DataFrame
|
|
67
|
+
Multi-indexed DataFrame containing plasma measurements with columns
|
|
68
|
+
labeled by ("M", "C", "S") for measurement, component, and species.
|
|
69
|
+
ions : pandas.Series of Ion objects
|
|
70
|
+
Dictionary-like access to individual ion species objects.
|
|
71
|
+
species : list of str
|
|
72
|
+
Available ion species identifiers in the plasma.
|
|
73
|
+
spacecraft : Spacecraft, optional
|
|
74
|
+
Spacecraft trajectory and velocity information.
|
|
75
|
+
auxiliary_data : pandas.DataFrame, optional
|
|
76
|
+
Additional measurements such as quality flags or derived parameters.
|
|
77
|
+
|
|
78
|
+
Notes
|
|
79
|
+
-----
|
|
80
|
+
Thermal speeds assume the relationship :math:`mw^2 = 2kT` where :math:`m`
|
|
81
|
+
is ion mass, :math:`w` is thermal speed, :math:`k` is Boltzmann's constant,
|
|
82
|
+
and :math:`T` is temperature.
|
|
83
|
+
|
|
84
|
+
The underlying data structure uses a three-level MultiIndex for columns:
|
|
85
|
+
- Level 0 (M): Measurement type ('n', 'v', 'w', 'b', etc.)
|
|
86
|
+
- Level 1 (C): Component ('x', 'y', 'z', 'par', 'per', etc.)
|
|
87
|
+
- Level 2 (S): Species identifier ('p1', 'a', 'o6', etc.)
|
|
88
|
+
|
|
89
|
+
Examples
|
|
90
|
+
--------
|
|
91
|
+
Create a plasma object from multi-species data:
|
|
92
|
+
|
|
93
|
+
>>> import pandas as pd
|
|
94
|
+
>>> import numpy as np
|
|
95
|
+
>>> # Create sample MultiIndex data
|
|
96
|
+
>>> epoch = pd.date_range('2023-01-01', periods=3, freq='1min')
|
|
97
|
+
>>> columns = pd.MultiIndex.from_tuples([
|
|
98
|
+
... ('n', '', 'p1'), ('v', 'x', 'p1'), ('v', 'y', 'p1'), ('v', 'z', 'p1'),
|
|
99
|
+
... ('n', '', 'a'), ('v', 'x', 'a'), ('v', 'y', 'a'), ('v', 'z', 'a'),
|
|
100
|
+
... ('w', 'par', 'p1'), ('w', 'per', 'p1'), ('w', 'par', 'a'), ('w', 'per', 'a'),
|
|
101
|
+
... ('b', 'x', ''), ('b', 'y', ''), ('b', 'z', '')
|
|
102
|
+
... ], names=['M', 'C', 'S'])
|
|
103
|
+
>>> data = pd.DataFrame(np.random.rand(3, len(columns)),
|
|
104
|
+
... index=epoch, columns=columns)
|
|
105
|
+
>>> plasma = Plasma(data, 'p1', 'a') # Protons and alphas
|
|
106
|
+
>>> type(plasma.p1).__name__ # Proton ion object
|
|
107
|
+
'Ion'
|
|
108
|
+
|
|
109
|
+
Calculate plasma physics parameters:
|
|
110
|
+
|
|
111
|
+
>>> beta = plasma.beta('p1') # Plasma beta for protons
|
|
112
|
+
>>> type(beta).__name__
|
|
113
|
+
'Tensor'
|
|
114
|
+
|
|
115
|
+
Idenfity ion species in plasma:
|
|
116
|
+
|
|
117
|
+
>>> plasma.species
|
|
118
|
+
['p1', 'a']
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
data,
|
|
124
|
+
*species,
|
|
125
|
+
spacecraft=None,
|
|
126
|
+
auxiliary_data=None,
|
|
127
|
+
log_plasma_stats=False,
|
|
128
|
+
):
|
|
129
|
+
r"""Initialize a :class:`Plasma` instance.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
data : :class:`pandas.DataFrame`
|
|
134
|
+
Contains the magnetic field and core ion moments. Columns are a
|
|
135
|
+
three-level :class:`~pandas.MultiIndex` labelled ``("M", "C", "S")``
|
|
136
|
+
for measurement, component, and species. The index should contain
|
|
137
|
+
datetime information, for example ``Epoch`` when loading from a CDF
|
|
138
|
+
file.
|
|
139
|
+
*species : str
|
|
140
|
+
Iterable of species contained in ``data``.
|
|
141
|
+
spacecraft : :class:`~solarwindpy.core.spacecraft.Spacecraft`, optional
|
|
142
|
+
Spacecraft trajectory and velocity information. If ``None``, the
|
|
143
|
+
Coulomb number :py:meth:`~Plasma.nc` method will raise a
|
|
144
|
+
:class:`ValueError`.
|
|
145
|
+
auxiliary_data : :class:`pandas.DataFrame`, optional
|
|
146
|
+
Additional measurements to carry with the plasma, for example data
|
|
147
|
+
quality flags. The column labelling scheme must match ``data``.
|
|
148
|
+
log_plasma_stats : bool, default ``False``
|
|
149
|
+
Log summary statistics when ``data`` is set.
|
|
150
|
+
|
|
151
|
+
Notes
|
|
152
|
+
-----
|
|
153
|
+
Thermal speeds assume :math:`mw^2 = 2kT`.
|
|
154
|
+
|
|
155
|
+
Examples
|
|
156
|
+
--------
|
|
157
|
+
>>> epoch = pd.Series({0: pd.to_datetime("1995-01-01"),
|
|
158
|
+
1: pd.to_datetime("2015-03-23"),
|
|
159
|
+
2: pd.to_datetime("2022-10-09")}, name="Epoch")
|
|
160
|
+
>>> data = {
|
|
161
|
+
("b", "x", ""): {0: 0.5, 1: 0.6, 2: 0.7},
|
|
162
|
+
("b", "y", ""): {0: -0.25, 1: -0.26, 2: 0.27},
|
|
163
|
+
("b", "z", ""): {0: 0.3, 1: 0.4, 2: -0.7},
|
|
164
|
+
("n", "", "a"): {0: 0.5, 1: 1.0, 2: 1.5},
|
|
165
|
+
("n", "", "p1"): {0: 1.0, 1: 2.0, 2: 3.0},
|
|
166
|
+
("v", "x", "a"): {0: 125.0, 1: 250.0, 2: 375.0},
|
|
167
|
+
("v", "x", "p1"): {0: 100.0, 1: 200.0, 2: 300.0},
|
|
168
|
+
("v", "y", "a"): {0: 250.0, 1: 375.0, 2: 750.0},
|
|
169
|
+
("v", "y", "p1"): {0: 200.0, 1: 300.0, 2: 600.0},
|
|
170
|
+
("v", "z", "a"): {0: 500.0, 1: 750.0, 2: 1000.0},
|
|
171
|
+
("v", "z", "p1"): {0: 400.0, 1: 600.0, 2: 800.0},
|
|
172
|
+
("w", "par", "a"): {0: 3.0, 1: 4.0, 2: 5.0},
|
|
173
|
+
("w", "par", "p1"): {0: 10.0, 1: 20.0, 2: 30.0},
|
|
174
|
+
("w", "per", "a"): {0: 7.0, 1: 9.0, 2: 10.0},
|
|
175
|
+
("w", "per", "p1"): {0: 7.0, 1: 26.0, 2: 28.0},
|
|
176
|
+
}
|
|
177
|
+
>>> data = pd.DataFrame.from_dict(data, orient="columns")
|
|
178
|
+
>>> data.columns.names = ["M", "C", "S"]
|
|
179
|
+
>>> data.index = epoch
|
|
180
|
+
>>> data.T
|
|
181
|
+
Epoch 1995-01-01 2015-03-23 2022-10-09
|
|
182
|
+
M C S
|
|
183
|
+
b x 0.50 0.60 0.70
|
|
184
|
+
y -0.25 -0.26 0.27
|
|
185
|
+
z 0.30 0.40 -0.70
|
|
186
|
+
n a 0.50 1.00 1.50
|
|
187
|
+
p1 1.00 2.00 3.00
|
|
188
|
+
v x a 125.00 250.00 375.00
|
|
189
|
+
p1 100.00 200.00 300.00
|
|
190
|
+
y a 250.00 375.00 750.00
|
|
191
|
+
p1 200.00 300.00 600.00
|
|
192
|
+
z a 500.00 750.00 1000.00
|
|
193
|
+
p1 400.00 600.00 800.00
|
|
194
|
+
w par a 3.00 4.00 5.00
|
|
195
|
+
p1 10.00 20.00 30.00
|
|
196
|
+
per a 7.00 9.00 10.00
|
|
197
|
+
p1 7.00 26.00 28.00
|
|
198
|
+
>>> plasma = Plasma(data, "a", "p1")
|
|
199
|
+
"""
|
|
200
|
+
self._init_logger()
|
|
201
|
+
self._set_species(*species)
|
|
202
|
+
self.set_log_plasma_stats(log_plasma_stats)
|
|
203
|
+
super(Plasma, self).__init__(data)
|
|
204
|
+
self._set_ions()
|
|
205
|
+
self.set_spacecraft(spacecraft)
|
|
206
|
+
self.set_auxiliary_data(auxiliary_data)
|
|
207
|
+
|
|
208
|
+
def __getattr__(self, attr):
|
|
209
|
+
if attr in self.ions.index:
|
|
210
|
+
return self.ions.loc[attr]
|
|
211
|
+
else:
|
|
212
|
+
return super(Plasma, self).__getattr__(attr)
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def epoch(self):
|
|
216
|
+
"""Time index of the plasma data.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
pandas.DatetimeIndex
|
|
221
|
+
Datetime index containing measurement timestamps.
|
|
222
|
+
|
|
223
|
+
Examples
|
|
224
|
+
--------
|
|
225
|
+
>>> plasma.epoch
|
|
226
|
+
DatetimeIndex(['1995-01-01', '2015-03-23', '2022-10-09'],
|
|
227
|
+
dtype='datetime64[ns]', name='Epoch', freq=None)
|
|
228
|
+
"""
|
|
229
|
+
return self.data.index
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def spacecraft(self):
|
|
233
|
+
r"""`Spacecraft` object stored in `plasma`."""
|
|
234
|
+
return self._spacecraft
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def sc(self):
|
|
238
|
+
r"""Shortcut to :py:attr:`spacecraft`."""
|
|
239
|
+
return self.spacecraft
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def auxiliary_data(self):
|
|
243
|
+
r"""Any data that does not fall into the following categories.
|
|
244
|
+
|
|
245
|
+
Epoch is index.
|
|
246
|
+
|
|
247
|
+
-magnetic field
|
|
248
|
+
-ion velocity
|
|
249
|
+
-ion number density
|
|
250
|
+
-ion thermal speed
|
|
251
|
+
"""
|
|
252
|
+
# try:
|
|
253
|
+
return self._auxiliary_data
|
|
254
|
+
|
|
255
|
+
# except AttributeError:
|
|
256
|
+
# raise AttributeError("No auxiliary data set.")
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def aux(self):
|
|
260
|
+
r"""Shortcut to :py:attr:`auxiliary_data`."""
|
|
261
|
+
return self.auxiliary_data
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def log_plasma_at_init(self):
|
|
265
|
+
"""Flag indicating whether to log plasma statistics during initialization.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
bool
|
|
270
|
+
True if plasma statistics should be logged at initialization.
|
|
271
|
+
|
|
272
|
+
See Also
|
|
273
|
+
--------
|
|
274
|
+
set_log_plasma_stats : Method to modify this setting
|
|
275
|
+
"""
|
|
276
|
+
return self._log_plasma_at_init
|
|
277
|
+
|
|
278
|
+
def set_log_plasma_stats(self, new):
|
|
279
|
+
"""Set flag for logging plasma statistics during initialization.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
new : bool
|
|
284
|
+
Whether to enable logging of plasma statistics.
|
|
285
|
+
|
|
286
|
+
Notes
|
|
287
|
+
-----
|
|
288
|
+
When enabled, summary statistics including density ranges, velocity
|
|
289
|
+
distributions, and magnetic field statistics are logged during
|
|
290
|
+
plasma initialization.
|
|
291
|
+
|
|
292
|
+
Examples
|
|
293
|
+
--------
|
|
294
|
+
>>> plasma.set_log_plasma_stats(True)
|
|
295
|
+
>>> plasma.log_plasma_at_init
|
|
296
|
+
True
|
|
297
|
+
"""
|
|
298
|
+
self._log_plasma_at_init = bool(new)
|
|
299
|
+
|
|
300
|
+
def save(
|
|
301
|
+
self,
|
|
302
|
+
fname,
|
|
303
|
+
dkey="FC",
|
|
304
|
+
sckey="SC",
|
|
305
|
+
akey="FC_AUX",
|
|
306
|
+
data_modifier_fcn=None,
|
|
307
|
+
sc_modifier_fcn=None,
|
|
308
|
+
aux_modifier_fcn=None,
|
|
309
|
+
):
|
|
310
|
+
r"""Save the plasma's data and aux DataFrame to an HDF5 file at `fname`.
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
fname: str or `pathlib.Path`.
|
|
315
|
+
File name pointing to the save location.
|
|
316
|
+
The typical use when creating a data file in `Create_Datafile.ipynb`
|
|
317
|
+
is `fname("swe", "h5", strip_date=True)`.
|
|
318
|
+
dkey: None
|
|
319
|
+
The HDF5 file key at which to store the data.
|
|
320
|
+
sckey: None
|
|
321
|
+
The HDF5 file key at which to store the spacecraft data.
|
|
322
|
+
akey: None
|
|
323
|
+
The HDF5 file key at which to store the auxiliary_data.
|
|
324
|
+
data_modifier_fcn: None, FunctionType
|
|
325
|
+
A function to modify the data saved, e.g. if you don't want to save
|
|
326
|
+
a specific species in the data file, you can pass.
|
|
327
|
+
|
|
328
|
+
def modify_data(data):
|
|
329
|
+
return data.drop("a", axis=1, level="S")
|
|
330
|
+
|
|
331
|
+
It can only take one argument, `data`.
|
|
332
|
+
spacecraft_modifier_fcn: None, FunctionType
|
|
333
|
+
A function to modifie the spacecraft data saved. See `data_modifier_fcn`
|
|
334
|
+
for syntax.
|
|
335
|
+
aux_modifier_fcn: None, FunctionType
|
|
336
|
+
A function to modify the auxiliary_data saved. See
|
|
337
|
+
`data_modifier_fcn` for syntax.
|
|
338
|
+
"""
|
|
339
|
+
from types import FunctionType
|
|
340
|
+
|
|
341
|
+
fname = str(fname)
|
|
342
|
+
data = self.data
|
|
343
|
+
sc = self.sc
|
|
344
|
+
aux = self.aux
|
|
345
|
+
|
|
346
|
+
if data_modifier_fcn is not None:
|
|
347
|
+
if not isinstance(data_modifier_fcn, FunctionType):
|
|
348
|
+
msg = (
|
|
349
|
+
"`modifier_fcn` must be a FunctionType. " "You passes '%s`."
|
|
350
|
+
) % type(data_modifier_fcn)
|
|
351
|
+
raise TypeError(msg)
|
|
352
|
+
data = data_modifier_fcn(data)
|
|
353
|
+
|
|
354
|
+
# Recalculate "w_scalar" on load, so no need to save.
|
|
355
|
+
data.drop("scalar", axis=1, level="C").to_hdf(fname, key=dkey)
|
|
356
|
+
self.logger.info(
|
|
357
|
+
"data saved\n{:<5} %s\n{:<5} %s\n{:<5} %s".format(
|
|
358
|
+
"file", "dkey", "shape"
|
|
359
|
+
),
|
|
360
|
+
fname,
|
|
361
|
+
dkey,
|
|
362
|
+
data.shape,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
msg = "`modifier_fcn` must be a FunctionType. " "You passes '%s`."
|
|
366
|
+
if sc is not None:
|
|
367
|
+
sc = sc.data
|
|
368
|
+
if sc_modifier_fcn is not None:
|
|
369
|
+
if not isinstance(sc_modifier_fcn, FunctionType):
|
|
370
|
+
raise TypeError(msg % type(sc_modifier_fcn))
|
|
371
|
+
sc = sc_modifier_fcn(sc)
|
|
372
|
+
|
|
373
|
+
sc.to_hdf(fname, key=sckey)
|
|
374
|
+
self.logger.info(
|
|
375
|
+
"spacecraft saved\n{:<5} %s\n{:<5} %s\n{:<5} %s".format(
|
|
376
|
+
"file", "sckey", "shape"
|
|
377
|
+
),
|
|
378
|
+
fname,
|
|
379
|
+
sckey,
|
|
380
|
+
sc.shape,
|
|
381
|
+
)
|
|
382
|
+
else:
|
|
383
|
+
self.logger.info("No spacecraft data to save")
|
|
384
|
+
|
|
385
|
+
if aux is not None:
|
|
386
|
+
if aux_modifier_fcn is not None:
|
|
387
|
+
if not isinstance(aux_modifier_fcn, FunctionType):
|
|
388
|
+
raise TypeError(msg % type(aux_modifier_fcn))
|
|
389
|
+
aux = aux_modifier_fcn(aux)
|
|
390
|
+
|
|
391
|
+
aux.to_hdf(fname, key=akey)
|
|
392
|
+
self.logger.info(
|
|
393
|
+
"aux saved\n{:<5} %s\n{:<5} %s\n{:<5} %s".format(
|
|
394
|
+
"file", "akey", "shape"
|
|
395
|
+
),
|
|
396
|
+
fname,
|
|
397
|
+
akey,
|
|
398
|
+
aux.shape,
|
|
399
|
+
)
|
|
400
|
+
else:
|
|
401
|
+
self.logger.info("No auxiliary data to save")
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
def load_from_file(
|
|
405
|
+
cls,
|
|
406
|
+
fname,
|
|
407
|
+
*species,
|
|
408
|
+
dkey="FC",
|
|
409
|
+
sckey="SC",
|
|
410
|
+
akey="FC_AUX",
|
|
411
|
+
sc_frame=None,
|
|
412
|
+
sc_name=None,
|
|
413
|
+
start=None,
|
|
414
|
+
stop=None,
|
|
415
|
+
**kwargs,
|
|
416
|
+
):
|
|
417
|
+
r"""Load data from an HDF5 file at `fname` and create a plasma.
|
|
418
|
+
|
|
419
|
+
Parameters
|
|
420
|
+
----------
|
|
421
|
+
fname: str or pathlib.Path
|
|
422
|
+
The file from which to load the data.
|
|
423
|
+
species: list-like of str
|
|
424
|
+
The species to load. If none are passed, they are automatically
|
|
425
|
+
selected from the data.
|
|
426
|
+
dkey: str, "FC"
|
|
427
|
+
The key for getting data from HDF5 file.
|
|
428
|
+
sckey: str, "SC"
|
|
429
|
+
The key for getting spacecraft data from the HDF5 file.
|
|
430
|
+
akey: str, "FC_AUX"
|
|
431
|
+
key for getting auxiliary data from HDF5 file.
|
|
432
|
+
start, stop: None, parsable by `pd.to_datetime`
|
|
433
|
+
If not None, time to start/stop for loading data.
|
|
434
|
+
kwargs:
|
|
435
|
+
Passed to `Plasma.__init__`.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
data = pd.read_hdf(fname, key=dkey)
|
|
439
|
+
data.columns.names = ["M", "C", "S"]
|
|
440
|
+
|
|
441
|
+
if start is not None or stop is not None:
|
|
442
|
+
data = data.loc[start:stop]
|
|
443
|
+
|
|
444
|
+
if not species:
|
|
445
|
+
species = [s for s in data.columns.get_level_values("S").unique() if s]
|
|
446
|
+
s_chk = [isinstance(s, str) for s in species]
|
|
447
|
+
if not np.all(s_chk):
|
|
448
|
+
msg = "Only string species allowed. Default or passed species: {}.".format(
|
|
449
|
+
s_chk
|
|
450
|
+
)
|
|
451
|
+
raise ValueError(msg)
|
|
452
|
+
|
|
453
|
+
log_at_init = kwargs.pop("log_plasma_stats", False)
|
|
454
|
+
plasma = cls(data, *species, log_plasma_stats=log_at_init, **kwargs)
|
|
455
|
+
|
|
456
|
+
plasma.logger.warning(
|
|
457
|
+
"Loaded plasma from file\nFile: %s\n\ndkey : %s\nshape : %s\nstart : %s\nstop : %s",
|
|
458
|
+
str(fname),
|
|
459
|
+
dkey,
|
|
460
|
+
data.shape,
|
|
461
|
+
data.index.min(),
|
|
462
|
+
data.index.max(),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if sckey:
|
|
466
|
+
sc = pd.read_hdf(fname, key=sckey)
|
|
467
|
+
sc.columns.names = ("M", "C")
|
|
468
|
+
|
|
469
|
+
if (sc_name is None) or (sc_frame is None):
|
|
470
|
+
raise ValueError(
|
|
471
|
+
"Must specify spacecraft name and frame\nname : %s\nframe: %s"
|
|
472
|
+
% (sc_name, sc_frame)
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if start is not None or stop is not None:
|
|
476
|
+
sc = sc.loc[data.index]
|
|
477
|
+
|
|
478
|
+
sc = spacecraft.Spacecraft(sc, sc_name, sc_frame)
|
|
479
|
+
|
|
480
|
+
plasma.set_spacecraft(sc)
|
|
481
|
+
plasma.logger.warning(
|
|
482
|
+
"Spacecraft data loaded\nsc_key: %s\nshape: %s", sckey, sc.data.shape
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
if akey:
|
|
486
|
+
aux = pd.read_hdf(fname, key=akey)
|
|
487
|
+
aux.columns.names = ("M", "C", "S")
|
|
488
|
+
|
|
489
|
+
if start is not None or stop is not None:
|
|
490
|
+
aux = aux.loc[data.index]
|
|
491
|
+
|
|
492
|
+
plasma.set_auxiliary_data(aux)
|
|
493
|
+
plasma.logger.warning(
|
|
494
|
+
"Auxiliary data loaded from file\nakey: %s\nshape: %s", akey, aux.shape
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
return plasma
|
|
498
|
+
|
|
499
|
+
def _set_species(self, *species):
|
|
500
|
+
r"""Initialize `species` property to make overriding `set_data` easier.
|
|
501
|
+
|
|
502
|
+
Initialize `species` property to make overriding `set_data`
|
|
503
|
+
easier.
|
|
504
|
+
"""
|
|
505
|
+
species = self._clean_species_for_setting(*species)
|
|
506
|
+
self._species = species
|
|
507
|
+
self.logger.debug("%s init with species %s", self.__class__.__name__, (species))
|
|
508
|
+
|
|
509
|
+
def _chk_species(self, *species):
|
|
510
|
+
r"""Internal tool to verify species string formats and availability.
|
|
511
|
+
|
|
512
|
+
Check the species in each :py:class:`Plasma` method call and ensure
|
|
513
|
+
they are available in the :py:attr:`ions`."""
|
|
514
|
+
species = self._conform_species(*species)
|
|
515
|
+
minimal_species = [s.split("+") for s in species]
|
|
516
|
+
minimal_species = np.unique([*itertools.chain(minimal_species)])
|
|
517
|
+
minimal_species = pd.Index(minimal_species)
|
|
518
|
+
|
|
519
|
+
# print("",
|
|
520
|
+
# "<_chk_species>",
|
|
521
|
+
# "<conformed>: {}".format(species),
|
|
522
|
+
# "<minimal>: {}".format(minimal_species),
|
|
523
|
+
# "<available>: {}".format(self.ions.index),
|
|
524
|
+
# sep="\n",
|
|
525
|
+
# end="\n\n")
|
|
526
|
+
|
|
527
|
+
unavailable = minimal_species.difference(self.ions.index)
|
|
528
|
+
|
|
529
|
+
if unavailable.any():
|
|
530
|
+
requested = ", ".join(sorted(species))
|
|
531
|
+
available = ", ".join(sorted(self.ions.index.values))
|
|
532
|
+
unavailable = ", ".join(unavailable.values)
|
|
533
|
+
msg = (
|
|
534
|
+
"Requested species unavailable.\n"
|
|
535
|
+
"Requested: %s\n"
|
|
536
|
+
"Available: %s\n"
|
|
537
|
+
"Unavailable: %s"
|
|
538
|
+
)
|
|
539
|
+
# print(msg % (requested, available, unavailable), flush=True, end="\n")
|
|
540
|
+
raise ValueError(msg % (requested, available, unavailable))
|
|
541
|
+
return species
|
|
542
|
+
|
|
543
|
+
@property
|
|
544
|
+
def species(self):
|
|
545
|
+
r"""Tuple of species contained in plasma."""
|
|
546
|
+
return self._species
|
|
547
|
+
|
|
548
|
+
@property
|
|
549
|
+
def ions(self):
|
|
550
|
+
r"""`pd.Series` containing the ions."""
|
|
551
|
+
return self._ions
|
|
552
|
+
|
|
553
|
+
def _set_ions(self):
|
|
554
|
+
species = self.species
|
|
555
|
+
if len(species) == 1:
|
|
556
|
+
species = species[0].split(",")
|
|
557
|
+
assert np.all(
|
|
558
|
+
["+" not in s for s in species]
|
|
559
|
+
), "Plasma.species can't contain '+'."
|
|
560
|
+
species = tuple(species)
|
|
561
|
+
|
|
562
|
+
ions_ = pd.Series({s: ions.Ion(self.data, s) for s in species})
|
|
563
|
+
self._ions = ions_
|
|
564
|
+
self._species = species
|
|
565
|
+
|
|
566
|
+
def drop_species(self, *species: str) -> "Plasma":
|
|
567
|
+
"""Return a new :class:`Plasma` without the specified species.
|
|
568
|
+
|
|
569
|
+
Parameters
|
|
570
|
+
----------
|
|
571
|
+
*species : str
|
|
572
|
+
Species to remove from the plasma.
|
|
573
|
+
|
|
574
|
+
Returns
|
|
575
|
+
-------
|
|
576
|
+
Plasma
|
|
577
|
+
A new plasma containing only the remaining species.
|
|
578
|
+
|
|
579
|
+
Raises
|
|
580
|
+
------
|
|
581
|
+
ValueError
|
|
582
|
+
If all species are removed.
|
|
583
|
+
"""
|
|
584
|
+
|
|
585
|
+
species_to_drop = self._chk_species(*species)
|
|
586
|
+
remaining = [s for s in self.species if s not in species_to_drop]
|
|
587
|
+
if not remaining:
|
|
588
|
+
raise ValueError("Must have >1 species. Can't have empty plasma.")
|
|
589
|
+
|
|
590
|
+
mask_keep = (
|
|
591
|
+
self.data.columns.get_level_values("S") == ""
|
|
592
|
+
) | self.data.columns.get_level_values("S").isin(remaining)
|
|
593
|
+
data = self.data.loc[:, mask_keep]
|
|
594
|
+
|
|
595
|
+
aux = None
|
|
596
|
+
if self.auxiliary_data is not None:
|
|
597
|
+
aux_mask = (
|
|
598
|
+
self.auxiliary_data.columns.get_level_values("S") == ""
|
|
599
|
+
) | self.auxiliary_data.columns.get_level_values("S").isin(remaining)
|
|
600
|
+
aux = self.auxiliary_data.loc[:, aux_mask]
|
|
601
|
+
|
|
602
|
+
new = Plasma(
|
|
603
|
+
data,
|
|
604
|
+
*remaining,
|
|
605
|
+
spacecraft=self.spacecraft,
|
|
606
|
+
auxiliary_data=aux,
|
|
607
|
+
log_plasma_stats=self.log_plasma_at_init,
|
|
608
|
+
)
|
|
609
|
+
return new
|
|
610
|
+
|
|
611
|
+
def set_spacecraft(self, new):
|
|
612
|
+
"""Set or update the spacecraft trajectory data.
|
|
613
|
+
|
|
614
|
+
Parameters
|
|
615
|
+
----------
|
|
616
|
+
new : Spacecraft or None
|
|
617
|
+
Spacecraft trajectory object containing position and velocity data.
|
|
618
|
+
Must have matching datetime index with plasma data.
|
|
619
|
+
|
|
620
|
+
Raises
|
|
621
|
+
------
|
|
622
|
+
AssertionError
|
|
623
|
+
If spacecraft index does not match plasma data index.
|
|
624
|
+
|
|
625
|
+
Notes
|
|
626
|
+
-----
|
|
627
|
+
The spacecraft data is required for calculating certain plasma physics
|
|
628
|
+
parameters such as Coulomb collision frequencies that depend on the
|
|
629
|
+
plasma frame transformation.
|
|
630
|
+
|
|
631
|
+
Examples
|
|
632
|
+
--------
|
|
633
|
+
>>> sc = Spacecraft(trajectory_data)
|
|
634
|
+
>>> plasma.set_spacecraft(sc)
|
|
635
|
+
>>> plasma.spacecraft.position # Access trajectory data
|
|
636
|
+
"""
|
|
637
|
+
assert isinstance(new, spacecraft.Spacecraft) or new is None
|
|
638
|
+
|
|
639
|
+
if new is not None:
|
|
640
|
+
assert isinstance(new.data.index, pd.DatetimeIndex)
|
|
641
|
+
assert new.data.index.equals(self.data.index)
|
|
642
|
+
assert new.data.columns.names == ("M", "C")
|
|
643
|
+
# Don't test spacecraft data duplicating plasma data b/c labels will
|
|
644
|
+
# overlap even though they represent different quantities because
|
|
645
|
+
# spacecraft only has a 2-level MultiIndex.
|
|
646
|
+
|
|
647
|
+
self._log_object_at_load(new.data if new is not None else new, "spacecraft")
|
|
648
|
+
self._spacecraft = new
|
|
649
|
+
|
|
650
|
+
def set_auxiliary_data(self, new):
|
|
651
|
+
"""Set or update auxiliary measurement data.
|
|
652
|
+
|
|
653
|
+
Parameters
|
|
654
|
+
----------
|
|
655
|
+
new : pandas.DataFrame or None
|
|
656
|
+
Additional measurements such as data quality flags, derived
|
|
657
|
+
parameters, or instrument-specific metadata. Must have matching
|
|
658
|
+
datetime index with plasma data.
|
|
659
|
+
|
|
660
|
+
Raises
|
|
661
|
+
------
|
|
662
|
+
AssertionError
|
|
663
|
+
If auxiliary data index does not match plasma data index.
|
|
664
|
+
|
|
665
|
+
Notes
|
|
666
|
+
-----
|
|
667
|
+
Auxiliary data provides additional context for plasma measurements
|
|
668
|
+
without being part of the core plasma physics calculations. Common
|
|
669
|
+
examples include quality flags, statistical uncertainties, or
|
|
670
|
+
instrument operational parameters.
|
|
671
|
+
|
|
672
|
+
Examples
|
|
673
|
+
--------
|
|
674
|
+
>>> quality_flags = pd.DataFrame({'quality': [0, 1, 0]},
|
|
675
|
+
... index=plasma.epoch)
|
|
676
|
+
>>> plasma.set_auxiliary_data(quality_flags)
|
|
677
|
+
>>> plasma.aux.quality # Access auxiliary data
|
|
678
|
+
"""
|
|
679
|
+
assert isinstance(new, pd.DataFrame) or new is None
|
|
680
|
+
|
|
681
|
+
if new is not None:
|
|
682
|
+
assert isinstance(new.index, pd.DatetimeIndex)
|
|
683
|
+
assert new.index.equals(self.data.index)
|
|
684
|
+
assert new.columns.names == ("M", "C", "S")
|
|
685
|
+
if new.columns.isin(self.data.columns).any():
|
|
686
|
+
raise ValueError("Auxiliary data should not duplicate plasma data")
|
|
687
|
+
|
|
688
|
+
self._log_object_at_load(new, "auxiliary_data")
|
|
689
|
+
self._auxiliary_data = new
|
|
690
|
+
|
|
691
|
+
def _log_object_at_load(self, data, name):
|
|
692
|
+
|
|
693
|
+
if data is None:
|
|
694
|
+
self.logger.info("No %s data passed to %s", name, self.__class__.__name__)
|
|
695
|
+
return None
|
|
696
|
+
|
|
697
|
+
elif self.log_plasma_at_init:
|
|
698
|
+
|
|
699
|
+
nan_frame = data.isna()
|
|
700
|
+
nan_info = pd.DataFrame(
|
|
701
|
+
{"count": nan_frame.sum(axis=0), "mean": nan_frame.mean(axis=0)}
|
|
702
|
+
)
|
|
703
|
+
# Log to DEBUG if no NaNs. Otherwise log to INFO.
|
|
704
|
+
if nan_info.any().any():
|
|
705
|
+
self.logger.info(
|
|
706
|
+
"%s %.0f spectra contain at least one NaN",
|
|
707
|
+
name,
|
|
708
|
+
nan_info.any(axis=1).sum(),
|
|
709
|
+
)
|
|
710
|
+
# self.logger.log(10 * int(1 + nan_info.any().any()),
|
|
711
|
+
# "plasma NaN info\n%s", nan_info.to_string())
|
|
712
|
+
self.logger.debug("%s NaN info\n%s", name, nan_info.to_string())
|
|
713
|
+
else:
|
|
714
|
+
self.logger.debug("%s does not contain NaNs", name)
|
|
715
|
+
|
|
716
|
+
pct = [0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99]
|
|
717
|
+
stats = (
|
|
718
|
+
pd.concat(
|
|
719
|
+
{
|
|
720
|
+
"lin": data.describe(percentiles=pct),
|
|
721
|
+
"log": data.applymap(np.log10).describe(percentiles=pct),
|
|
722
|
+
},
|
|
723
|
+
axis=0,
|
|
724
|
+
)
|
|
725
|
+
.unstack(level=0)
|
|
726
|
+
.sort_index(axis=0)
|
|
727
|
+
.sort_index(axis=1)
|
|
728
|
+
.T
|
|
729
|
+
)
|
|
730
|
+
self.logger.debug(
|
|
731
|
+
"%s stats\n%s\n%s",
|
|
732
|
+
name,
|
|
733
|
+
stats.loc[:, ["count", "mean", "std"]].to_string(),
|
|
734
|
+
stats.drop(["count", "mean", "std"], axis=1).to_string(),
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def set_data(self, new):
|
|
738
|
+
r"""Set the data and log statistics about it."""
|
|
739
|
+
# assert isinstance(new, pd.DataFrame)
|
|
740
|
+
super(Plasma, self).set_data(new)
|
|
741
|
+
|
|
742
|
+
new = new.reorder_levels(["M", "C", "S"], axis=1).sort_index(axis=1)
|
|
743
|
+
# new = new.sort_index(axis=1, inplace=True)
|
|
744
|
+
assert new.columns.names == ["M", "C", "S"]
|
|
745
|
+
|
|
746
|
+
# assert isinstance(new.index, pd.DatetimeIndex)
|
|
747
|
+
# if not new.index.is_monotonic:
|
|
748
|
+
# self.logger.warning(
|
|
749
|
+
# r"""A non-monotonic DatetimeIndex typically indicates the presence of bad data. This will impact perfomance and prevent some DatetimeIndex-dependent functionality from working."""
|
|
750
|
+
# )
|
|
751
|
+
|
|
752
|
+
# These are the only quantities we want in plasma.
|
|
753
|
+
# TODO: move `theta_rms`, `mag_rms` and anything not common to
|
|
754
|
+
# multiple spacecraft to `auxiliary_data`. (20190216)
|
|
755
|
+
tk_plasma = pd.IndexSlice[
|
|
756
|
+
["b", "n", "v", "w"],
|
|
757
|
+
["", "x", "y", "z", "per", "par"],
|
|
758
|
+
list(self.species) + [""],
|
|
759
|
+
]
|
|
760
|
+
|
|
761
|
+
data = new.loc[:, tk_plasma].sort_index(axis=1)
|
|
762
|
+
dropped = new.drop(data.columns, axis=1)
|
|
763
|
+
data = data.loc[:, ~data.columns.duplicated()]
|
|
764
|
+
|
|
765
|
+
coeff = pd.Series({"per": 2.0, "par": 1.0}) / 3.0
|
|
766
|
+
|
|
767
|
+
w = (
|
|
768
|
+
data.loc[:, pd.IndexSlice["w", ["par", "per"]]]
|
|
769
|
+
.pow(2)
|
|
770
|
+
.multiply(coeff, axis=1, level="C")
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# w = (
|
|
774
|
+
# data.w.drop("scalar", axis=1, level="C")
|
|
775
|
+
# .pow(2)
|
|
776
|
+
# .multiply(coeff, axis=1, level="C")
|
|
777
|
+
# )
|
|
778
|
+
|
|
779
|
+
# TODO: test `skipna=False` to ensure we don't accidentially create valid data
|
|
780
|
+
# where there is none. Actually, not possible as we are combining along
|
|
781
|
+
# "S".
|
|
782
|
+
|
|
783
|
+
# Workaround for `skipna=False` bug. (20200814)
|
|
784
|
+
# w = w.sum(axis=1, level="S", skipna=False).applymap(np.sqrt)
|
|
785
|
+
# Changed to new groupby method (20250611)
|
|
786
|
+
w = w.T.groupby("S").sum().T.pow(0.5)
|
|
787
|
+
# w_is_finite = w.notna().all(axis=1, level="S")
|
|
788
|
+
# w = w.sum(axis=1, level="S").applymap(np.sqrt)
|
|
789
|
+
# w = w.where(w_is_finite, axis=1, level="S")
|
|
790
|
+
|
|
791
|
+
# TODO: can probably just `w.columns.map(lambda x: ("w", "scalar", x))`
|
|
792
|
+
w.columns = w.columns.to_series().apply(lambda x: ("w", "scalar", x))
|
|
793
|
+
w.columns = self.mi_tuples(w.columns)
|
|
794
|
+
|
|
795
|
+
# data = pd.concat([data, w], axis=1, sort=True)
|
|
796
|
+
data = pd.concat([data, w], axis=1, sort=False).sort_index(
|
|
797
|
+
axis=1
|
|
798
|
+
) # .sort_idex(axis=0)
|
|
799
|
+
|
|
800
|
+
data.columns = self.mi_tuples(data.columns)
|
|
801
|
+
data = data.sort_index(axis=1)
|
|
802
|
+
|
|
803
|
+
self._data = data
|
|
804
|
+
self.logger.debug(
|
|
805
|
+
"plasma shape: %s\nstart: %s\nstop: %s",
|
|
806
|
+
data.shape,
|
|
807
|
+
data.index.min(),
|
|
808
|
+
data.index.max(),
|
|
809
|
+
)
|
|
810
|
+
if dropped.columns.values.any():
|
|
811
|
+
self.logger.info(
|
|
812
|
+
"columns dropped from plasma\n%s",
|
|
813
|
+
[str(c) for c in dropped.columns.values],
|
|
814
|
+
)
|
|
815
|
+
else:
|
|
816
|
+
self.logger.info("no columns dropped from plasma")
|
|
817
|
+
|
|
818
|
+
self._bfield = vector.BField(data.b.xs("", axis=1, level="S"))
|
|
819
|
+
|
|
820
|
+
self._log_object_at_load(data, "plasma")
|
|
821
|
+
|
|
822
|
+
@property
|
|
823
|
+
def bfield(self):
|
|
824
|
+
r"""Magnetic field data."""
|
|
825
|
+
return self._bfield
|
|
826
|
+
|
|
827
|
+
@property
|
|
828
|
+
def b(self):
|
|
829
|
+
r"""Shortcut for :py:attr:`bfield`."""
|
|
830
|
+
return self.bfield
|
|
831
|
+
|
|
832
|
+
def number_density(self, *species, skipna=True):
|
|
833
|
+
r"""Get the plasma number densities.
|
|
834
|
+
|
|
835
|
+
Parameters
|
|
836
|
+
----------
|
|
837
|
+
species: str
|
|
838
|
+
Each species is a string. If only one string is passed, it can
|
|
839
|
+
contain "+". If this is the case, the species are summed over and
|
|
840
|
+
a pd.Series is returned. Otherwise, the individual quantities are
|
|
841
|
+
returned as a pd.DataFrame.
|
|
842
|
+
skipna: bool, default True
|
|
843
|
+
Follows `pd.DataFrame.sum` convention. If True, NA excluded from
|
|
844
|
+
results. If False, NA propagates. False is helpful to identify
|
|
845
|
+
when a species is not measured using NaNs in its number density.
|
|
846
|
+
|
|
847
|
+
Returns
|
|
848
|
+
-------
|
|
849
|
+
n: pd.Series or pd.DataFrame
|
|
850
|
+
See Parameters for more info.
|
|
851
|
+
"""
|
|
852
|
+
slist = self._chk_species(*species)
|
|
853
|
+
|
|
854
|
+
n = {s: self.ions.loc[s].n for s in slist}
|
|
855
|
+
n = pd.concat(n, axis=1, names=["S"], sort=True)
|
|
856
|
+
|
|
857
|
+
if len(species) == 1:
|
|
858
|
+
n = n.sum(axis=1, skipna=skipna)
|
|
859
|
+
n.name = species[0]
|
|
860
|
+
|
|
861
|
+
return n
|
|
862
|
+
|
|
863
|
+
def n(self, *species, skipna=True):
|
|
864
|
+
r"""Shortcut to :py:meth:`number_density`."""
|
|
865
|
+
return self.number_density(*species, skipna=skipna)
|
|
866
|
+
|
|
867
|
+
def mass_density(self, *species):
|
|
868
|
+
r"""Get the plasma mass densities.
|
|
869
|
+
|
|
870
|
+
Parameters
|
|
871
|
+
----------
|
|
872
|
+
species: str
|
|
873
|
+
Each species is a string. If only one string is passed, it can
|
|
874
|
+
contain "+". If this is the case, the species are summed over and
|
|
875
|
+
a pd.Series is returned. Otherwise, the individual quantities are
|
|
876
|
+
returned as a pd.DataFrame.
|
|
877
|
+
|
|
878
|
+
Returns
|
|
879
|
+
-------
|
|
880
|
+
rho: pd.Series or pd.DataFrame
|
|
881
|
+
See Parameters for more info.
|
|
882
|
+
"""
|
|
883
|
+
slist = self._chk_species(*species)
|
|
884
|
+
|
|
885
|
+
rho = {s: self.ions.loc[s].rho for s in slist}
|
|
886
|
+
rho = pd.concat(rho, axis=1, names=["S"], sort=True)
|
|
887
|
+
|
|
888
|
+
if len(species) == 1:
|
|
889
|
+
rho = rho.sum(axis=1)
|
|
890
|
+
rho.name = species[0]
|
|
891
|
+
return rho
|
|
892
|
+
|
|
893
|
+
def rho(self, *species):
|
|
894
|
+
r"""Shortcut to :py:meth:`mass_density`."""
|
|
895
|
+
return self.mass_density(*species)
|
|
896
|
+
|
|
897
|
+
def thermal_speed(self, *species):
|
|
898
|
+
r"""Get the thermal speed.
|
|
899
|
+
|
|
900
|
+
Parameters
|
|
901
|
+
----------
|
|
902
|
+
species: str
|
|
903
|
+
Each species is a string. A total species ("s0+s1+...") cannot be passed
|
|
904
|
+
because the result is physically amibguous.
|
|
905
|
+
|
|
906
|
+
Returns
|
|
907
|
+
-------
|
|
908
|
+
w: pd.Series or pd.DataFrame
|
|
909
|
+
See Parameters for more info.
|
|
910
|
+
"""
|
|
911
|
+
if np.any(["+" in s for s in species]):
|
|
912
|
+
raise NotImplementedError(
|
|
913
|
+
"The result of a total species thermal speed is physically ambiguous"
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
slist = self._chk_species(*species)
|
|
917
|
+
w = {s: self.ions.loc[s].thermal_speed.data for s in slist}
|
|
918
|
+
w = pd.concat(w, axis=1, names=["S"], sort=True)
|
|
919
|
+
w = w.reorder_levels(["C", "S"], axis=1).sort_index(axis=1)
|
|
920
|
+
|
|
921
|
+
if len(species) == 1:
|
|
922
|
+
# w = w.sum(axis=1, level="C")
|
|
923
|
+
w = w.T.groupby(level="C").sum().T
|
|
924
|
+
|
|
925
|
+
return w
|
|
926
|
+
|
|
927
|
+
def w(self, *species):
|
|
928
|
+
r"""Shortcut to :py:meth:`thermal_speed`."""
|
|
929
|
+
return self.thermal_speed(*species)
|
|
930
|
+
|
|
931
|
+
def pth(self, *species):
|
|
932
|
+
r"""Get the thermal pressure.
|
|
933
|
+
|
|
934
|
+
Parameters
|
|
935
|
+
----------
|
|
936
|
+
species: str
|
|
937
|
+
Each species is a string. If only one string is passed, it can
|
|
938
|
+
contain "+". If this is the case, the species are summed over and
|
|
939
|
+
a pd.Series is returned. Otherwise, the individual quantities are
|
|
940
|
+
returned as a pd.DataFrame.
|
|
941
|
+
|
|
942
|
+
Returns
|
|
943
|
+
-------
|
|
944
|
+
pth: pd.Series or pd.DataFrame
|
|
945
|
+
See Parameters for more info.
|
|
946
|
+
"""
|
|
947
|
+
slist = self._chk_species(*species)
|
|
948
|
+
include_dynamic = False
|
|
949
|
+
if include_dynamic:
|
|
950
|
+
raise NotImplementedError
|
|
951
|
+
|
|
952
|
+
pth = {s: self.ions.loc[s].pth for s in slist}
|
|
953
|
+
pth = pd.concat(pth, axis=1, names=["S"], sort=True)
|
|
954
|
+
pth = pth.reorder_levels(["C", "S"], axis=1).sort_index(axis=1)
|
|
955
|
+
|
|
956
|
+
if len(species) == 1:
|
|
957
|
+
pth = pth.T.groupby("C").sum().T
|
|
958
|
+
# pth["S"] = species[0]
|
|
959
|
+
# pth = pth.set_index("S", append=True).unstack()
|
|
960
|
+
# pth = pth.reorder_levels(["C", "S"], axis=1).sort_index(axis=1)
|
|
961
|
+
return pth
|
|
962
|
+
|
|
963
|
+
def temperature(self, *species):
|
|
964
|
+
r"""Get the thermal temperature.
|
|
965
|
+
|
|
966
|
+
Parameters
|
|
967
|
+
----------
|
|
968
|
+
species: str
|
|
969
|
+
Each species is a string. If only one string is passed, it can
|
|
970
|
+
contain "+". If this is the case, the species are summed over and
|
|
971
|
+
a pd.Series is returned. Otherwise, the individual quantities are
|
|
972
|
+
returned as a pd.DataFrame.
|
|
973
|
+
|
|
974
|
+
Returns
|
|
975
|
+
-------
|
|
976
|
+
temp: pd.Series or pd.DataFrame
|
|
977
|
+
See Parameters for more info.
|
|
978
|
+
"""
|
|
979
|
+
slist = self._chk_species(*species)
|
|
980
|
+
temp = {s: self.ions.loc[s].temperature for s in slist}
|
|
981
|
+
temp = pd.concat(temp, axis=1, names=["S"], sort=True)
|
|
982
|
+
temp = temp.reorder_levels(["C", "S"], axis=1).sort_index(axis=1)
|
|
983
|
+
|
|
984
|
+
if len(species) == 1:
|
|
985
|
+
temp = temp.T.groupby("C").sum().T
|
|
986
|
+
# temp["S"] = species[0]
|
|
987
|
+
# temp = temp.set_index("S", append=True).unstack()
|
|
988
|
+
# temp = temp.reorder_levels(["C", "S"], axis=1).sort_index(axis=1)
|
|
989
|
+
return temp
|
|
990
|
+
|
|
991
|
+
def beta(self, *species):
|
|
992
|
+
r"""Get perpendicular, parallel, and scalar plasma beta.
|
|
993
|
+
|
|
994
|
+
Parameters
|
|
995
|
+
----------
|
|
996
|
+
species: str
|
|
997
|
+
Each species is a string. Species handling controlled by :py:meth:`pth`.
|
|
998
|
+
|
|
999
|
+
Returns
|
|
1000
|
+
-------
|
|
1001
|
+
beta: :py:class:`pd.DataFrame`
|
|
1002
|
+
See Parameters for more info.
|
|
1003
|
+
|
|
1004
|
+
Notes
|
|
1005
|
+
-----
|
|
1006
|
+
In uncertain units, the NRL Plasma Formulary (2016) defined
|
|
1007
|
+
:math:`\beta`:
|
|
1008
|
+
|
|
1009
|
+
:math:`\beta = \frac{8 \pi n k_B T}{B^2} = \frac{2 k_b T / m}{B^2 / 4 \phi \rho}`
|
|
1010
|
+
|
|
1011
|
+
and the Alfven speed as:
|
|
1012
|
+
|
|
1013
|
+
:math:`C_A^2 = B^2 / 4 \pi \rho`.
|
|
1014
|
+
|
|
1015
|
+
I define thermal speed as:
|
|
1016
|
+
|
|
1017
|
+
:math:`w^2 = \frac{2 k_B T}{m}`.
|
|
1018
|
+
|
|
1019
|
+
Combining these equations, we get:
|
|
1020
|
+
|
|
1021
|
+
:math:`\beta = w^2 / C_A^2`,
|
|
1022
|
+
|
|
1023
|
+
which is independent of dimensional constants. Given I define
|
|
1024
|
+
:math:`p_{th} = \frac{1}{2} \rho w^2` and :math:`C_A^2 = \frac{1}{\mu_0}B^2 \rho` in SI units, I can
|
|
1025
|
+
rewrite :math:`\beta`
|
|
1026
|
+
|
|
1027
|
+
:math:`\beta = \frac{2 p_{th}}{\rho} \frac{\mu_0 \rho}{B^2} = \frac{2 \mu_0 p_{th}}{B^2}`.
|
|
1028
|
+
"""
|
|
1029
|
+
slist = self._chk_species(*species) # noqa: F841
|
|
1030
|
+
include_dynamic = False
|
|
1031
|
+
if include_dynamic:
|
|
1032
|
+
raise NotImplementedError
|
|
1033
|
+
|
|
1034
|
+
pth = self.pth(*species)
|
|
1035
|
+
bsq = self.bfield.mag.pow(2)
|
|
1036
|
+
beta = pth.divide(bsq, axis=0)
|
|
1037
|
+
|
|
1038
|
+
units = self.units.pth / (self.units.b**2.0)
|
|
1039
|
+
coeff = 2.0 * self.constants.misc.mu0 * units
|
|
1040
|
+
beta *= coeff
|
|
1041
|
+
return beta
|
|
1042
|
+
|
|
1043
|
+
def anisotropy(self, *species):
|
|
1044
|
+
r"""Pressure anisotropy.
|
|
1045
|
+
|
|
1046
|
+
Note that for a single species, the pressure anisotropy is just the
|
|
1047
|
+
temperature anisotropy.
|
|
1048
|
+
|
|
1049
|
+
Parameters
|
|
1050
|
+
----------
|
|
1051
|
+
species: str
|
|
1052
|
+
Each species is a string. Species handling is primarily controlled
|
|
1053
|
+
by :py:meth:`pth`.
|
|
1054
|
+
|
|
1055
|
+
Returns
|
|
1056
|
+
-------
|
|
1057
|
+
ani: :py:class:`pd.Series` or :py:class:`pd.DataFrame`
|
|
1058
|
+
See Parameters for more info.
|
|
1059
|
+
"""
|
|
1060
|
+
pth = self.pth(*species).drop("scalar", axis=1)
|
|
1061
|
+
|
|
1062
|
+
include_dynamic = False
|
|
1063
|
+
if include_dynamic:
|
|
1064
|
+
raise NotImplementedError
|
|
1065
|
+
pdv = self.pdv(*species)
|
|
1066
|
+
pth.loc[:, "par"] = pth.loc[:, "par"].add(pdv, axis=0)
|
|
1067
|
+
|
|
1068
|
+
exp = pd.Series({"par": -1, "per": 1})
|
|
1069
|
+
|
|
1070
|
+
if len(species) > 1:
|
|
1071
|
+
# if "S" in pth.columns.names:
|
|
1072
|
+
# ani = pth.pow(exp, axis=1, level="C").product(axis=1, level="S")
|
|
1073
|
+
ani = pth.pow(exp, axis=1, level="C").T.groupby(level="S").prod().T
|
|
1074
|
+
else:
|
|
1075
|
+
ani = pth.pow(exp, axis=1).product(axis=1)
|
|
1076
|
+
ani.name = species[0]
|
|
1077
|
+
|
|
1078
|
+
return ani
|
|
1079
|
+
|
|
1080
|
+
def velocity(self, *species, project_m2q=False):
|
|
1081
|
+
r"""Get an ion velocity or calculate the center-of-mass velocity.
|
|
1082
|
+
|
|
1083
|
+
Parameters
|
|
1084
|
+
----------
|
|
1085
|
+
species: str
|
|
1086
|
+
Each species is a string. If only one string is passed and contains
|
|
1087
|
+
"+", return a pd.Series containing the center-of-mass velocity
|
|
1088
|
+
:py:class:`~solarwindpy.core.vector.Vector`. If contains a single species,
|
|
1089
|
+
return that ion's velocity.
|
|
1090
|
+
project_m2q: bool, False
|
|
1091
|
+
If True, project velocity by :math:`\sqrt{m/q}`. Disables center-of-
|
|
1092
|
+
mass species.
|
|
1093
|
+
|
|
1094
|
+
Returns
|
|
1095
|
+
-------
|
|
1096
|
+
velocity: :py:class:`pd.Series` or :py:class:`pd.DataFrame`
|
|
1097
|
+
"""
|
|
1098
|
+
stuple = self._chk_species(*species)
|
|
1099
|
+
|
|
1100
|
+
# print("", "<Module>", sep="\n")
|
|
1101
|
+
|
|
1102
|
+
if len(stuple) == 1:
|
|
1103
|
+
# print("<Module.ion>")
|
|
1104
|
+
s = stuple[0]
|
|
1105
|
+
v = self.ions.loc[s].velocity
|
|
1106
|
+
if project_m2q:
|
|
1107
|
+
m2q = np.sqrt(
|
|
1108
|
+
self.constants.m_in_mp[s] / self.constants.charge_states[s]
|
|
1109
|
+
)
|
|
1110
|
+
v = v.data.multiply(m2q)
|
|
1111
|
+
v = vector.Vector(v)
|
|
1112
|
+
|
|
1113
|
+
elif project_m2q:
|
|
1114
|
+
raise NotImplementedError(
|
|
1115
|
+
"""A multi-species velocity is not valid when projecting by sqrt(m/q).
|
|
1116
|
+
species: {}
|
|
1117
|
+
""".format(
|
|
1118
|
+
species
|
|
1119
|
+
)
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
else:
|
|
1123
|
+
# print("<Module.sum>")
|
|
1124
|
+
v = self.ions.loc[list(stuple)].apply(lambda x: x.velocity)
|
|
1125
|
+
if len(species) == 1:
|
|
1126
|
+
rhos = self.mass_density(*stuple)
|
|
1127
|
+
v = pd.concat(
|
|
1128
|
+
v.apply(lambda x: x.cartesian).to_dict(),
|
|
1129
|
+
axis=1,
|
|
1130
|
+
names=["S"],
|
|
1131
|
+
sort=True,
|
|
1132
|
+
)
|
|
1133
|
+
rv = (
|
|
1134
|
+
v.multiply(rhos, axis=1, level="S").T.groupby(level="C").sum().T
|
|
1135
|
+
) # sum(axis=1, level="C")
|
|
1136
|
+
v = rv.divide(rhos.sum(axis=1), axis=0)
|
|
1137
|
+
v = vector.Vector(v)
|
|
1138
|
+
|
|
1139
|
+
return v
|
|
1140
|
+
|
|
1141
|
+
def v(self, *species, project_m2q=False):
|
|
1142
|
+
r"""Shortcut to `velocity`."""
|
|
1143
|
+
return self.velocity(*species, project_m2q=project_m2q)
|
|
1144
|
+
|
|
1145
|
+
def dv(self, s0, s1, project_m2q=False):
|
|
1146
|
+
r"""Calculate the differential flow between species `s0` and `s1`.
|
|
1147
|
+
|
|
1148
|
+
Calculate the differential flow between species `s0` and
|
|
1149
|
+
species `s1`: :math:`v_{s0} - v_{s1}`.
|
|
1150
|
+
|
|
1151
|
+
Parameters
|
|
1152
|
+
----------
|
|
1153
|
+
s0, s1: str
|
|
1154
|
+
If either species contains a "+", the center-of-mass velocity
|
|
1155
|
+
for the indicated species is used.
|
|
1156
|
+
project_m2q: bool, False
|
|
1157
|
+
If True, project each speed by :math:`\sqrt{m/q}`. Disables center-
|
|
1158
|
+
of-mass species.
|
|
1159
|
+
|
|
1160
|
+
Returns
|
|
1161
|
+
-------
|
|
1162
|
+
dv: vector.Vector
|
|
1163
|
+
|
|
1164
|
+
See Also
|
|
1165
|
+
--------
|
|
1166
|
+
vector.Vector
|
|
1167
|
+
"""
|
|
1168
|
+
if s0 == s1:
|
|
1169
|
+
msg = (
|
|
1170
|
+
"The differential flow between a species and itself "
|
|
1171
|
+
"is identically zero.\ns0: %s\ns1: %s"
|
|
1172
|
+
)
|
|
1173
|
+
raise NotImplementedError(msg % (s0, s1))
|
|
1174
|
+
|
|
1175
|
+
v0 = self.velocity(s0, project_m2q=project_m2q).cartesian
|
|
1176
|
+
v1 = self.velocity(s1, project_m2q=project_m2q).cartesian
|
|
1177
|
+
|
|
1178
|
+
dv = v0.subtract(v1)
|
|
1179
|
+
dv = vector.Vector(dv)
|
|
1180
|
+
|
|
1181
|
+
return dv
|
|
1182
|
+
|
|
1183
|
+
def pdynamic(self, *species, project_m2q=False):
|
|
1184
|
+
r"""Calculate the dynamic or drift pressure for the given species.
|
|
1185
|
+
|
|
1186
|
+
:math:`p_{\tilde{v}} = 0.5 \sum_i \rho_i (v_i - v_\mathrm{com})^2`
|
|
1187
|
+
|
|
1188
|
+
The calculation is done in the plasma frame.
|
|
1189
|
+
|
|
1190
|
+
Parameters
|
|
1191
|
+
----------
|
|
1192
|
+
species: list-like of str
|
|
1193
|
+
List-like of individual species, e.g. ["a", "p1"].
|
|
1194
|
+
Can NOT be a list-like including sums, e.g. ["a", "p1+p2"].
|
|
1195
|
+
project_m2q: bool, False
|
|
1196
|
+
If True, project the velocities by :math:`\sqrt{m/q}`. Allows for only
|
|
1197
|
+
two species to be passed and takes the differential flow between them.
|
|
1198
|
+
|
|
1199
|
+
Returns
|
|
1200
|
+
-------
|
|
1201
|
+
pdv: pd.Series
|
|
1202
|
+
Dynamic pressure due to `species`.
|
|
1203
|
+
"""
|
|
1204
|
+
stuple = self._chk_species(*species)
|
|
1205
|
+
if len(stuple) == 1:
|
|
1206
|
+
msg = "Must have >1 species to calculate dynamic pressure.\nRequested: {}"
|
|
1207
|
+
raise ValueError(msg.format(species))
|
|
1208
|
+
|
|
1209
|
+
const = 0.5 * self.units.rho * (self.units.dv**2.0) / self.units.pth
|
|
1210
|
+
|
|
1211
|
+
if not project_m2q:
|
|
1212
|
+
# Calculate as m*v
|
|
1213
|
+
scom = "+".join(species)
|
|
1214
|
+
rho_i = self.mass_density(*stuple)
|
|
1215
|
+
dv_i = pd.concat(
|
|
1216
|
+
{s: self.dv(s, scom).cartesian for s in stuple},
|
|
1217
|
+
axis=1,
|
|
1218
|
+
names="S",
|
|
1219
|
+
sort=True,
|
|
1220
|
+
)
|
|
1221
|
+
dvsq_i = dv_i.pow(2.0).T.groupby(level="S").sum().T
|
|
1222
|
+
dvsq_rho_i = dvsq_i.multiply(rho_i, axis=1, level="S")
|
|
1223
|
+
pdv = dvsq_rho_i.sum(axis=1)
|
|
1224
|
+
|
|
1225
|
+
elif len(stuple) == 2:
|
|
1226
|
+
# Can only have 2 species with `project_m2q`.
|
|
1227
|
+
dvsq = (
|
|
1228
|
+
self.dv(*stuple, project_m2q=project_m2q).cartesian.pow(2).sum(axis=1)
|
|
1229
|
+
)
|
|
1230
|
+
rho_i = self.mass_density(*stuple)
|
|
1231
|
+
mu = rho_i.product(axis=1).divide(rho_i.sum(axis=1), axis=0)
|
|
1232
|
+
pdv = dvsq.multiply(mu, axis=0)
|
|
1233
|
+
|
|
1234
|
+
pdv = pdv.multiply(const)
|
|
1235
|
+
pdv.name = "pdynamic"
|
|
1236
|
+
|
|
1237
|
+
# print("",
|
|
1238
|
+
# "<Module>",
|
|
1239
|
+
# "<stuple>: {}".format(stuple),
|
|
1240
|
+
# "<scom> %s" % scom,
|
|
1241
|
+
# "<const> %s" % const,
|
|
1242
|
+
# "<rho_i>", type(rho_i), rho_i,
|
|
1243
|
+
# "<dv_i>", type(dv_i), dv_i,
|
|
1244
|
+
# "<dvsq_i>", type(dvsq_i), dvsq_i,
|
|
1245
|
+
# "<dvsq_rho_i>", type(dvsq_rho_i), dvsq_rho_i,
|
|
1246
|
+
# "<pdv>", type(pdv), pdv,
|
|
1247
|
+
# sep="\n",
|
|
1248
|
+
# end="\n\n")
|
|
1249
|
+
|
|
1250
|
+
# dvsq_rho_i = dvsq_i.multiply(rho_i, axis=1, level="S")
|
|
1251
|
+
# pdv = dvsq_rho_i.sum(axis=1).multiply(const)
|
|
1252
|
+
# pdv = const * dv_i.pow(2.0).sum(axis=1,
|
|
1253
|
+
# level="S").multiply(rhi_i,
|
|
1254
|
+
# axis=1,
|
|
1255
|
+
# level="S").sum(axis=1)
|
|
1256
|
+
# pdv.name = "pdynamic"
|
|
1257
|
+
|
|
1258
|
+
# print(
|
|
1259
|
+
# "<dvsq_rho_i>", type(dvsq_rho_i), dvsq_rho_i,
|
|
1260
|
+
# "<pdv>", type(pdv), pdv,
|
|
1261
|
+
# sep="\n",
|
|
1262
|
+
# end="\n\n")
|
|
1263
|
+
|
|
1264
|
+
return pdv
|
|
1265
|
+
|
|
1266
|
+
def pdv(self, *species, project_m2q=False):
|
|
1267
|
+
r"""Shortcut to :py:meth:`pdynamic`."""
|
|
1268
|
+
return self.pdynamic(*species, project_m2q=project_m2q)
|
|
1269
|
+
|
|
1270
|
+
def sound_speed(self, *species):
|
|
1271
|
+
r"""Calculate the sound speed.
|
|
1272
|
+
|
|
1273
|
+
Parameters
|
|
1274
|
+
----------
|
|
1275
|
+
species: str
|
|
1276
|
+
TODO: What controls species?
|
|
1277
|
+
|
|
1278
|
+
Returns
|
|
1279
|
+
-------
|
|
1280
|
+
cs: pd.DataFrame or pd.Series depending on `species` inputs.
|
|
1281
|
+
"""
|
|
1282
|
+
slist = self._chk_species(*species)
|
|
1283
|
+
rho = self.mass_density(*species) * self.units.rho
|
|
1284
|
+
pth = self.pth(*species) * self.units.pth
|
|
1285
|
+
|
|
1286
|
+
pth = pth.loc[:, "scalar"]
|
|
1287
|
+
|
|
1288
|
+
# gamma = self.units_constants.misc.loc["gamma"] # should be 5.0/3.0
|
|
1289
|
+
gamma = self.constants.polytropic_index["scalar"] # should be 5/3
|
|
1290
|
+
cs = pth.divide(rho, axis=0).multiply(gamma).pow(0.5) / self.units.cs
|
|
1291
|
+
|
|
1292
|
+
# raise NotImplementedError(
|
|
1293
|
+
# "How do we name this species? Need to figure out species processing up top."
|
|
1294
|
+
# )
|
|
1295
|
+
if len(species) == 1:
|
|
1296
|
+
cs.name = species[0]
|
|
1297
|
+
else:
|
|
1298
|
+
assert cs.columns.isin(slist).all()
|
|
1299
|
+
|
|
1300
|
+
return cs
|
|
1301
|
+
|
|
1302
|
+
def cs(self, *species):
|
|
1303
|
+
r"""Shortcut to :py:meth:`sound_speed`."""
|
|
1304
|
+
return self.sound_speed(*species)
|
|
1305
|
+
|
|
1306
|
+
def ca(self, *species):
|
|
1307
|
+
r"""Calculate the isotropic MHD Alfven speed.
|
|
1308
|
+
|
|
1309
|
+
Parameters
|
|
1310
|
+
----------
|
|
1311
|
+
species: str
|
|
1312
|
+
Species controlled by :py:meth:`mass_density`
|
|
1313
|
+
|
|
1314
|
+
Returns
|
|
1315
|
+
-------
|
|
1316
|
+
ca: pd.DataFrame or pd.Series depending on `species` inputs.
|
|
1317
|
+
"""
|
|
1318
|
+
stuple = self._chk_species(*species) # noqa: F841
|
|
1319
|
+
|
|
1320
|
+
rho = self.mass_density(*species)
|
|
1321
|
+
b = self.bfield.mag
|
|
1322
|
+
|
|
1323
|
+
units = self.units
|
|
1324
|
+
mu0 = self.constants.misc.mu0
|
|
1325
|
+
coeff = units.b / (np.sqrt(units.rho * mu0) * units.ca)
|
|
1326
|
+
ca = rho.pow(-0.5).multiply(b, axis=0) * coeff
|
|
1327
|
+
|
|
1328
|
+
if len(species) == 1:
|
|
1329
|
+
ca.name = species[0]
|
|
1330
|
+
|
|
1331
|
+
# print_inline_debug_info = False
|
|
1332
|
+
# if print_inline_debug_info:
|
|
1333
|
+
# print("",
|
|
1334
|
+
# "<Module>",
|
|
1335
|
+
# "<species>", stuple,
|
|
1336
|
+
# "<b>", type(b), b,
|
|
1337
|
+
# "<rho>", type(rho), rho,
|
|
1338
|
+
# "<ca>", type(ca), ca,
|
|
1339
|
+
# sep="\n")
|
|
1340
|
+
|
|
1341
|
+
return ca
|
|
1342
|
+
|
|
1343
|
+
def afsq(self, *species, pdynamic=False):
|
|
1344
|
+
r"""Calculate the square of anisotropy factor.
|
|
1345
|
+
|
|
1346
|
+
:math:`AF^2 = 1 + \frac{\mu_0}{B^s}\left(p_\perp - p_\parallel - p_{\tilde{v}}\right)`
|
|
1347
|
+
|
|
1348
|
+
N.B. Because of the :math:`1 +`, afsq(s0, s1).sum(axis=1) is not the
|
|
1349
|
+
same as afsq(s0+s1). The two are related by:
|
|
1350
|
+
|
|
1351
|
+
afsq.(s0+s1) = 1 + (afsq(s0, s1) - 1).sum(axis=1)
|
|
1352
|
+
|
|
1353
|
+
Parameters
|
|
1354
|
+
----------
|
|
1355
|
+
species: str
|
|
1356
|
+
Each species is a string. If only one string is passed, it can
|
|
1357
|
+
contain "+". If this is the case, the species are summed over and
|
|
1358
|
+
a pd.Series is returned. Otherwise, the individual quantities are
|
|
1359
|
+
returned as a pd.DataFrame.
|
|
1360
|
+
pydnamic: bool, str
|
|
1361
|
+
If str, the component of the dynamic pressure to use when
|
|
1362
|
+
calculating :math:`p_{\tilde{v}}`.
|
|
1363
|
+
|
|
1364
|
+
Returns
|
|
1365
|
+
-------
|
|
1366
|
+
afsq: pd.Series or pd.DataFrame depending on the len(species).
|
|
1367
|
+
"""
|
|
1368
|
+
if pdynamic:
|
|
1369
|
+
raise NotImplementedError(
|
|
1370
|
+
"Youngest beams analysis shows "
|
|
1371
|
+
"that dynamic pressure is probably not useful."
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
# The following is used to specifiy whether column levels
|
|
1375
|
+
# need to be aligned when multiple species are present.
|
|
1376
|
+
multi_species = len(species) > 1
|
|
1377
|
+
|
|
1378
|
+
bsq = self.bfield.cartesian.pow(2.0).sum(axis=1)
|
|
1379
|
+
|
|
1380
|
+
pth = self.pth(*species)
|
|
1381
|
+
pth = pth.drop("scalar", axis=1)
|
|
1382
|
+
|
|
1383
|
+
sum_coeff = pd.Series({"per": 1, "par": -1})
|
|
1384
|
+
dp = pth.multiply(sum_coeff, axis=1, level="C" if multi_species else None)
|
|
1385
|
+
|
|
1386
|
+
# The following level kwarg controls returning a DataFrame
|
|
1387
|
+
# of the various species or a single result for one species.
|
|
1388
|
+
# My guess is that following this line, we'd insert the subtraction
|
|
1389
|
+
# of the dynamic pressure with the appropriate alignment of the
|
|
1390
|
+
# species as necessary.
|
|
1391
|
+
# dp = dp.sum(axis=1, level="S" if multi_species else None)
|
|
1392
|
+
if multi_species:
|
|
1393
|
+
dp = dp.T.groupby(level="S").sum().T
|
|
1394
|
+
else:
|
|
1395
|
+
dp = dp.sum(axis=1)
|
|
1396
|
+
|
|
1397
|
+
mu0 = self.constants.misc.mu0
|
|
1398
|
+
coeff = mu0 * self.units.pth / (self.units.b**2.0)
|
|
1399
|
+
|
|
1400
|
+
afsq = 1.0 + (dp.divide(bsq, axis=0) * coeff)
|
|
1401
|
+
|
|
1402
|
+
if len(species) == 1:
|
|
1403
|
+
afsq.name = species[0]
|
|
1404
|
+
|
|
1405
|
+
# print(""
|
|
1406
|
+
# "<Module>",
|
|
1407
|
+
# "<species>: {}".format(species),
|
|
1408
|
+
# "<bsq>", type(bsq), bsq,
|
|
1409
|
+
# "<coeff>", type(coeff), coeff,
|
|
1410
|
+
# "<pth>", type(pth), pth,
|
|
1411
|
+
# "<dp>", type(dp), dp,
|
|
1412
|
+
# "<afsq>", type(afsq), afsq,
|
|
1413
|
+
# "",
|
|
1414
|
+
# sep="\n")
|
|
1415
|
+
|
|
1416
|
+
return afsq
|
|
1417
|
+
|
|
1418
|
+
def caani(self, *species, pdynamic=False):
|
|
1419
|
+
r"""
|
|
1420
|
+
Calculate the anisotropic MHD Alfven speed:
|
|
1421
|
+
|
|
1422
|
+
:math:`C_{A;Ani} = C_A\sqrt{AFSQ}`
|
|
1423
|
+
|
|
1424
|
+
Parameters
|
|
1425
|
+
----------
|
|
1426
|
+
species: str
|
|
1427
|
+
Each species is a string. If only one string is passed, it can
|
|
1428
|
+
contain "+". In either case, all species are summed over and
|
|
1429
|
+
a pd.Series is returned. This addresses complications from the
|
|
1430
|
+
`stuple = self._chk_species(*species)` mass densities in Ca and AFSQ,
|
|
1431
|
+
the latter via :py:meth:`pth`.
|
|
1432
|
+
pydnamic: bool, str
|
|
1433
|
+
If str, the component of the dynamic pressure to use when
|
|
1434
|
+
calculating :math:`p_{\tilde{v}}`.
|
|
1435
|
+
|
|
1436
|
+
Returns
|
|
1437
|
+
-------
|
|
1438
|
+
caani: pd.Series
|
|
1439
|
+
Only pd.Series is returned because of the combination of mass
|
|
1440
|
+
density and pressure terms in the CaAni equation.
|
|
1441
|
+
|
|
1442
|
+
See Also
|
|
1443
|
+
--------
|
|
1444
|
+
ca, afsq
|
|
1445
|
+
"""
|
|
1446
|
+
stuple = self._chk_species(*species)
|
|
1447
|
+
ssum = "+".join(stuple)
|
|
1448
|
+
|
|
1449
|
+
ca = self.ca(ssum)
|
|
1450
|
+
afsq = self.afsq(ssum, pdynamic=pdynamic)
|
|
1451
|
+
caani = ca.multiply(afsq.pipe(np.sqrt))
|
|
1452
|
+
|
|
1453
|
+
# print("",
|
|
1454
|
+
# "<Module>",
|
|
1455
|
+
# "<species>: {}".format(ssum),
|
|
1456
|
+
# "<ca>", type(ca), ca,
|
|
1457
|
+
# "<afsq>", type(afsq), afsq,
|
|
1458
|
+
# "<caani>", type(caani), caani,
|
|
1459
|
+
# "",
|
|
1460
|
+
# sep="\n")
|
|
1461
|
+
|
|
1462
|
+
return caani
|
|
1463
|
+
|
|
1464
|
+
def lnlambda(self, s0, s1):
|
|
1465
|
+
r"""Calculate the Coulomb logarithm between species s0 and s1.
|
|
1466
|
+
|
|
1467
|
+
:math:`\ln_\lambda_{i,i} = 29.9 - \ln(\frac{z_0 * z_1 * (a_0 + a_1)}{a_0 * T_1 + a_1 * T_0} \sqrt{\frac{n_0 z_0^2}{T_0} + \frac{n_1 z_1^2}{T_1}})`
|
|
1468
|
+
|
|
1469
|
+
Parameters
|
|
1470
|
+
----------
|
|
1471
|
+
species: str
|
|
1472
|
+
Each species is a string. It cannot be a sum of species,
|
|
1473
|
+
nor can it be an iterable of species.
|
|
1474
|
+
|
|
1475
|
+
Returns
|
|
1476
|
+
-------
|
|
1477
|
+
lnlambda: pd.Series
|
|
1478
|
+
Only `pd.Series` is returned because Coulomb require
|
|
1479
|
+
species alignment in such a fashion that array
|
|
1480
|
+
operations using `pd.DataFrame` alignment won't work.
|
|
1481
|
+
|
|
1482
|
+
See Also
|
|
1483
|
+
--------
|
|
1484
|
+
nuc
|
|
1485
|
+
"""
|
|
1486
|
+
s0 = self._chk_species(s0)
|
|
1487
|
+
s1 = self._chk_species(s1)
|
|
1488
|
+
|
|
1489
|
+
if len(s0) > 1 or len(s1) > 1:
|
|
1490
|
+
msg = (
|
|
1491
|
+
"`lnlambda` can only calculate with individual s0 and "
|
|
1492
|
+
"s1 species.\ns0: %s\ns1: %s"
|
|
1493
|
+
)
|
|
1494
|
+
raise ValueError(msg % (s0, s1))
|
|
1495
|
+
|
|
1496
|
+
s0 = s0[0]
|
|
1497
|
+
s1 = s1[0]
|
|
1498
|
+
|
|
1499
|
+
constants = self.constants
|
|
1500
|
+
units = self.units
|
|
1501
|
+
|
|
1502
|
+
z0 = constants.charge_states.loc[s0]
|
|
1503
|
+
z1 = constants.charge_states.loc[s1]
|
|
1504
|
+
|
|
1505
|
+
a0 = constants.m_amu.loc[s0]
|
|
1506
|
+
a1 = constants.m_amu.loc[s1]
|
|
1507
|
+
|
|
1508
|
+
n0 = self.ions.loc[s0].n * units.n
|
|
1509
|
+
n1 = self.ions.loc[s1].n * units.n
|
|
1510
|
+
|
|
1511
|
+
T0 = self.ions.loc[s0].temperature.scalar * units.temperature * constants.kb.eV
|
|
1512
|
+
T1 = self.ions.loc[s1].temperature.scalar * units.temperature * constants.kb.eV
|
|
1513
|
+
|
|
1514
|
+
r0 = n0.multiply(z0**2.0).divide(T0, axis=0)
|
|
1515
|
+
r1 = n1.multiply(z1**2.0).divide(T1, axis=0)
|
|
1516
|
+
right = r0.add(r1).pipe(np.sqrt)
|
|
1517
|
+
|
|
1518
|
+
left = z0 * z1 * (a0 + a1) / (a0 * T1).add(a1 * T0, axis=0)
|
|
1519
|
+
|
|
1520
|
+
lnlambda = (29.9 - np.log(left * right)) / units.lnlambda
|
|
1521
|
+
lnlambda.name = "%s,%s" % (s0, s1)
|
|
1522
|
+
|
|
1523
|
+
# print("",
|
|
1524
|
+
# "<Module>",
|
|
1525
|
+
# "<ions>", type(self.ions), self.ions,
|
|
1526
|
+
# "<s0, s1>: %s, %s" % (s0, s1),
|
|
1527
|
+
# "<z0>", z0,
|
|
1528
|
+
# "<z1>", z1,
|
|
1529
|
+
# "<n0>", type(n0), n0,
|
|
1530
|
+
# "<n1>", type(n1), n1,
|
|
1531
|
+
# "<T0>", type(T0), T0,
|
|
1532
|
+
# "<T1>", type(T1), T1,
|
|
1533
|
+
# "<r0>", type(r0), r0,
|
|
1534
|
+
# "<r1>", type(r1), r1,
|
|
1535
|
+
# "<right>", type(right), right,
|
|
1536
|
+
# "<left>", type(left), left,
|
|
1537
|
+
# "<lnlambda>", type(lnlambda), lnlambda,
|
|
1538
|
+
# "<Module Done>",
|
|
1539
|
+
# "",
|
|
1540
|
+
# sep="\n")
|
|
1541
|
+
|
|
1542
|
+
return lnlambda
|
|
1543
|
+
|
|
1544
|
+
def nuc(self, sa, sb, both_species=True):
|
|
1545
|
+
r"""Calculate the momentum collision rate following [1].
|
|
1546
|
+
|
|
1547
|
+
Parameters
|
|
1548
|
+
----------
|
|
1549
|
+
sa, sb: str
|
|
1550
|
+
The test, field particle species. Each can only identify a single
|
|
1551
|
+
ion species and it cannot be an iterable of lists, etc.
|
|
1552
|
+
both_species: bool
|
|
1553
|
+
If True, calculate the effective collision rate for a
|
|
1554
|
+
two-ion-species plasma following Eq. (23). Otherwise, calculate
|
|
1555
|
+
it following Eq. (18).
|
|
1556
|
+
|
|
1557
|
+
Returns
|
|
1558
|
+
-------
|
|
1559
|
+
nu: pd.Series
|
|
1560
|
+
|
|
1561
|
+
Notes
|
|
1562
|
+
-----
|
|
1563
|
+
If nu.name is "sa-sb", then `both_species=False` in calclulation.
|
|
1564
|
+
If nu.name is "sa+sb", then `both_species=True`.
|
|
1565
|
+
|
|
1566
|
+
See Also
|
|
1567
|
+
--------
|
|
1568
|
+
lnlambda, nc
|
|
1569
|
+
|
|
1570
|
+
References
|
|
1571
|
+
----------
|
|
1572
|
+
[1] Hernández, R., & Marsch, E. (1985). Collisional time scales for
|
|
1573
|
+
temperature and velocity exchange between drifting Maxwellians.
|
|
1574
|
+
Journal of Geophysical Research, 90(A11), 11062.
|
|
1575
|
+
<https://doi.org/10.1029/JA090iA11p11062>.
|
|
1576
|
+
"""
|
|
1577
|
+
from scipy.special import erf
|
|
1578
|
+
|
|
1579
|
+
sa = self._chk_species(sa)
|
|
1580
|
+
sb = self._chk_species(sb)
|
|
1581
|
+
|
|
1582
|
+
if len(sa) > 1 or len(sb) > 1:
|
|
1583
|
+
msg = (
|
|
1584
|
+
"`nuc` can only calculate with individual `sa` and "
|
|
1585
|
+
"`sb` species.\nsa: %s\nsb: %s"
|
|
1586
|
+
)
|
|
1587
|
+
raise ValueError(msg % (sa, sb))
|
|
1588
|
+
|
|
1589
|
+
sa, sb = sa[0], sb[0]
|
|
1590
|
+
|
|
1591
|
+
units = self.units
|
|
1592
|
+
constants = self.constants
|
|
1593
|
+
|
|
1594
|
+
qabsq = constants.charges.loc[[sa, sb]].pow(2).product()
|
|
1595
|
+
ma = constants.m.loc[sa]
|
|
1596
|
+
masses = constants.m.loc[[sa, sb]]
|
|
1597
|
+
mu = masses.product() / masses.sum()
|
|
1598
|
+
coeff = qabsq / (4.0 * np.pi * constants.misc.e0**2.0 * ma * mu)
|
|
1599
|
+
|
|
1600
|
+
lnlambda = self.lnlambda(sa, sb) * units.lnlambda
|
|
1601
|
+
nb = self.ions.loc[sb].n * units.n
|
|
1602
|
+
|
|
1603
|
+
w = pd.concat(
|
|
1604
|
+
{s: self.ions.loc[s].w.data.par for s in [sa, sb]}, axis=1, sort=True
|
|
1605
|
+
)
|
|
1606
|
+
wab = w.pow(2.0).sum(axis=1).pipe(np.sqrt) * units.w
|
|
1607
|
+
|
|
1608
|
+
dv = self.dv(sa, sb).magnitude * units.dv
|
|
1609
|
+
dvw = dv.divide(wab, axis=0)
|
|
1610
|
+
|
|
1611
|
+
# longitudinal diffusion rate.
|
|
1612
|
+
ldr1 = erf(dvw)
|
|
1613
|
+
ldr2 = dvw.multiply((2.0 / np.sqrt(np.pi)) * np.exp(-1 * dvw.pow(2.0)), axis=0)
|
|
1614
|
+
ldr = dvw.pow(-3.0).multiply(ldr1.subtract(ldr2, axis=0), axis=0)
|
|
1615
|
+
|
|
1616
|
+
nuab = coeff * nb.multiply(lnlambda, axis=0).multiply(ldr, axis=0).multiply(
|
|
1617
|
+
wab.pow(-3.0), axis=0
|
|
1618
|
+
)
|
|
1619
|
+
nuab /= units.nuc
|
|
1620
|
+
|
|
1621
|
+
# print("",
|
|
1622
|
+
# "<Module>",
|
|
1623
|
+
# "<species>: {}".format((sa, sb)),
|
|
1624
|
+
# "<ma>", type(ma), ma,
|
|
1625
|
+
# "<masses>", type(masses), masses,
|
|
1626
|
+
# "<mu>", type(mu), mu,
|
|
1627
|
+
# "<qab^2>", type(qabsq), qabsq,
|
|
1628
|
+
# "<qa^2 qb^2 / 4 pi e0^2 ma mu>", type(coeff), coeff,
|
|
1629
|
+
# "<w>", type(w), w,
|
|
1630
|
+
# "<wab>", type(wab), wab,
|
|
1631
|
+
# "<lnlambda>", type(lnlambda), lnlambda,
|
|
1632
|
+
# "<nb>", type(nb), nb,
|
|
1633
|
+
# "<wab>", type(wab), wab,
|
|
1634
|
+
# "<dv>", type(dv), dv,
|
|
1635
|
+
# "<dv/wab>", type(dvw), dvw,
|
|
1636
|
+
#
|
|
1637
|
+
# "<erf(dv/wab)>", type(ldr1), ldr1,
|
|
1638
|
+
# "<(dv/wab) * 2/sqrt(pi) * exp(-(dv/wab)^2)>", type(ldr2), ldr2,
|
|
1639
|
+
# "<transverse diffusion rate>", type(ldr), ldr,
|
|
1640
|
+
# "<nuab>", type(nuab), nuab,
|
|
1641
|
+
# sep="\n")
|
|
1642
|
+
|
|
1643
|
+
if both_species:
|
|
1644
|
+
exp = pd.Series({sa: 1.0, sb: -1.0})
|
|
1645
|
+
rho_ratio = pd.concat(
|
|
1646
|
+
{s: self.mass_density(s) for s in [sa, sb]}, axis=1, sort=True
|
|
1647
|
+
)
|
|
1648
|
+
rho_ratio = rho_ratio.pow(exp, axis=1).product(axis=1)
|
|
1649
|
+
nuba = nuab.multiply(rho_ratio, axis=0)
|
|
1650
|
+
nu = nuab.add(nuba, axis=0)
|
|
1651
|
+
# nu.name = "%s+%s" % (sa, sb)
|
|
1652
|
+
nu.name = f"{sa}+{sb}"
|
|
1653
|
+
# print(
|
|
1654
|
+
# "<rho_a/rho_b>", type(rho_ratio), rho_ratio,
|
|
1655
|
+
# "<nuba>", type(nuba), nuba,
|
|
1656
|
+
# sep="\n")
|
|
1657
|
+
else:
|
|
1658
|
+
nu = nuab
|
|
1659
|
+
# nu.name = "%s-%s" % (sa, sb)
|
|
1660
|
+
nu.name = f"{sa}-{sb}"
|
|
1661
|
+
|
|
1662
|
+
# print(
|
|
1663
|
+
# "<both_species> %s" % both_species,
|
|
1664
|
+
# "<nu>", type(nu), nu,
|
|
1665
|
+
# "",
|
|
1666
|
+
# sep="\n")
|
|
1667
|
+
|
|
1668
|
+
return nu
|
|
1669
|
+
|
|
1670
|
+
def nc(self, sa, sb, both_species=True):
|
|
1671
|
+
r"""Calculate the Coulomb number between species `sa` and `sb`.
|
|
1672
|
+
|
|
1673
|
+
Parameters
|
|
1674
|
+
----------
|
|
1675
|
+
sa, sb: str
|
|
1676
|
+
Species identifying the ions to use in calculation. Can't be a
|
|
1677
|
+
combination of things like "s0+s1", "s0,s1", nor ("s0", "s1").
|
|
1678
|
+
both_species: bool
|
|
1679
|
+
Passed to `nuc`. If True, calculate the two-ion-plasma collision frequency.
|
|
1680
|
+
|
|
1681
|
+
Returns
|
|
1682
|
+
-------
|
|
1683
|
+
nc: pd.Series
|
|
1684
|
+
Coulomb number
|
|
1685
|
+
|
|
1686
|
+
See Also
|
|
1687
|
+
--------
|
|
1688
|
+
nuc, lnlambda
|
|
1689
|
+
"""
|
|
1690
|
+
sa = self._chk_species(sa)
|
|
1691
|
+
sb = self._chk_species(sb)
|
|
1692
|
+
|
|
1693
|
+
if len(sa) > 1 or len(sb) > 1:
|
|
1694
|
+
msg = (
|
|
1695
|
+
"`nc` can only calculate with individual `sa` and "
|
|
1696
|
+
"`sb` species.\nsa: %s\nsb: %s"
|
|
1697
|
+
)
|
|
1698
|
+
raise ValueError(msg % (sa, sb))
|
|
1699
|
+
|
|
1700
|
+
sa, sb = sa[0], sb[0]
|
|
1701
|
+
|
|
1702
|
+
sc = self.spacecraft
|
|
1703
|
+
if sc is None:
|
|
1704
|
+
msg = "Plasma doesn't contain spacecraft data. Can't calculate Coulomb number."
|
|
1705
|
+
raise ValueError(msg)
|
|
1706
|
+
|
|
1707
|
+
r = sc.distance2sun * self.units.distance2sun
|
|
1708
|
+
# r = self.constants.misc.loc["1AU [m]"] - (
|
|
1709
|
+
# self.gse.x * self.constants.misc.loc["Re [m]"]
|
|
1710
|
+
# )
|
|
1711
|
+
vsw = self.velocity("+".join(self.species)).mag * self.units.v
|
|
1712
|
+
tau_exp = r.divide(vsw, axis=0)
|
|
1713
|
+
|
|
1714
|
+
nuc = self.nuc(sa, sb, both_species=both_species) * self.units.nuc
|
|
1715
|
+
|
|
1716
|
+
nc = nuc.multiply(tau_exp, axis=0) / self.units.nc
|
|
1717
|
+
nc.name = nuc.name
|
|
1718
|
+
# Nc name should be handled by nuc name conventions.
|
|
1719
|
+
|
|
1720
|
+
# print("",
|
|
1721
|
+
# "<Module>",
|
|
1722
|
+
# "<species>: {}".format((sa, sb)),
|
|
1723
|
+
# "<both species>: %s" % both_species,
|
|
1724
|
+
# "<r>", type(r), r,
|
|
1725
|
+
# "<vsw>", type(vsw), vsw,
|
|
1726
|
+
# "<tau_exp>", type(tau_exp), tau_exp,
|
|
1727
|
+
# "<nuc>", type(nuc), nuc,
|
|
1728
|
+
# "<nc>", type(nc), nc,
|
|
1729
|
+
# "",
|
|
1730
|
+
# sep="\n")
|
|
1731
|
+
|
|
1732
|
+
return nc
|
|
1733
|
+
|
|
1734
|
+
def vdf_ratio(self, beam="p2", core="p1"):
|
|
1735
|
+
r"""Calculate the ratio of the VDFs at the beam velocity.
|
|
1736
|
+
|
|
1737
|
+
Calculate the ratio of a bi-Maxwellian proton beam to a bi-Maxwellian
|
|
1738
|
+
proton core VDF at the peak beam velocity.
|
|
1739
|
+
|
|
1740
|
+
To avoid overflow erros, we return ln(ratio).
|
|
1741
|
+
|
|
1742
|
+
The VDF for species :math:`i` at velocity :math:`v_j` is:
|
|
1743
|
+
|
|
1744
|
+
:math:`f_i(v_j) = \frac{n_i}{(\pi w_i ^2)^{3/2}} \exp[ -(\frac{v_j - v_i}{w_i})^2]`
|
|
1745
|
+
|
|
1746
|
+
The beam to core VDF ratio evaluated at the proton beam velocity is:
|
|
1747
|
+
|
|
1748
|
+
:math:`\frac{f_2}{f_1}|_{v_2} = \frac{n_2}{n_1} ( \frac{w_1}{w_2} )^3 \exp[ (\frac{v_2 - v_1}{w_1})^2 ]`
|
|
1749
|
+
|
|
1750
|
+
where :math:`n` is the number density, :math:`w` gives the thermal
|
|
1751
|
+
speed, and :math:`u` is the bulk velocity.
|
|
1752
|
+
|
|
1753
|
+
In the case of a Bimaxwellian, we :math:`w^3 = w_\parallel w_\perp^2`
|
|
1754
|
+
:math:`(\frac{v - v_i}{w_i})^2 = (\frac{v - v_i}{w_i})_\parallel^2 + (\frac{v - v_i}{w_i})_\perp^2`.
|
|
1755
|
+
|
|
1756
|
+
Parameters
|
|
1757
|
+
----------
|
|
1758
|
+
plasma : pd.DataFrame
|
|
1759
|
+
Contains the number densities, vector velocities, and thermal speeds
|
|
1760
|
+
of the beam and core species.
|
|
1761
|
+
beam : str, "p2"
|
|
1762
|
+
The beam population, defaults to proton beams.
|
|
1763
|
+
core : str, "p1"
|
|
1764
|
+
The core population, defaults to proton core.
|
|
1765
|
+
|
|
1766
|
+
Returns
|
|
1767
|
+
-------
|
|
1768
|
+
f2f1 : pd.Series
|
|
1769
|
+
Natural logarithm of the beam to core VDF ratio.
|
|
1770
|
+
|
|
1771
|
+
Notes
|
|
1772
|
+
-----
|
|
1773
|
+
This routine was written for Faraday cup data quality validation, so
|
|
1774
|
+
alpha particle velocities are projected with by :math:`\sqrt{2.0}` to
|
|
1775
|
+
the velocity window in which they are measured.
|
|
1776
|
+
"""
|
|
1777
|
+
beam = self._chk_species(beam)
|
|
1778
|
+
core = self._chk_species(core)
|
|
1779
|
+
|
|
1780
|
+
if len(beam) > 1:
|
|
1781
|
+
raise ValueError(
|
|
1782
|
+
"""VDFs are evaluated on a species-by-species basis. Beam `{}` is invalid.""".format(
|
|
1783
|
+
beam
|
|
1784
|
+
)
|
|
1785
|
+
)
|
|
1786
|
+
if len(core) > 1:
|
|
1787
|
+
raise ValueError(
|
|
1788
|
+
"""VDFs are evaluated on a species-by-species basis. Core `{}` is invalid.""".format(
|
|
1789
|
+
core
|
|
1790
|
+
)
|
|
1791
|
+
)
|
|
1792
|
+
|
|
1793
|
+
beam = beam[0]
|
|
1794
|
+
core = core[0]
|
|
1795
|
+
|
|
1796
|
+
n1 = self.data.xs(("n", "", core), axis=1)
|
|
1797
|
+
n2 = self.data.xs(("n", "", beam), axis=1)
|
|
1798
|
+
|
|
1799
|
+
w = self.w(beam, core).drop("scalar", axis=1, level="C")
|
|
1800
|
+
w1_par = w.par.loc[:, core]
|
|
1801
|
+
w1_per = w.per.loc[:, core]
|
|
1802
|
+
w2_par = w.par.loc[:, beam]
|
|
1803
|
+
w2_per = w.per.loc[:, beam]
|
|
1804
|
+
|
|
1805
|
+
dv = self.dv(beam, core, project_m2q=True).project(self.b)
|
|
1806
|
+
dvw = dv.divide(w.xs(core, axis=1, level="S")).pow(2).sum(axis=1)
|
|
1807
|
+
|
|
1808
|
+
nbar = n2 / n1
|
|
1809
|
+
wbar = (w1_par / w2_par).multiply((w1_per / w2_per).pow(2), axis=0)
|
|
1810
|
+
coef = nbar.multiply(wbar, axis=0).apply(np.log)
|
|
1811
|
+
# f2f1 = nbar * wbar * f2f1
|
|
1812
|
+
f2f1 = coef.add(dvw, axis=0)
|
|
1813
|
+
|
|
1814
|
+
assert isinstance(f2f1, pd.Series)
|
|
1815
|
+
sbc = "%s/%s" % (beam, core)
|
|
1816
|
+
f2f1.name = sbc
|
|
1817
|
+
|
|
1818
|
+
# print("",
|
|
1819
|
+
# "<Module>",
|
|
1820
|
+
# "<species>: {},{}".format(beam, core),
|
|
1821
|
+
# "<ni>", type(n1), n1,
|
|
1822
|
+
# "<nj>", type(n2), n2,
|
|
1823
|
+
# "<nbar>", type(nbar), nbar,
|
|
1824
|
+
# "<w1_par>", type(w1_par), w1_par,
|
|
1825
|
+
# "<w1_per>", type(w1_per), w1_per,
|
|
1826
|
+
# "<w2_par>", type(w2_par), w2_par,
|
|
1827
|
+
# "<w2_per>", type(w2_per), w2_per,
|
|
1828
|
+
# "<wbar>", type(wbar), wbar,
|
|
1829
|
+
# "<coef>", type(coef), coef,
|
|
1830
|
+
# "<dv>", type(dv), dv,
|
|
1831
|
+
# "<dvw>", type(dvw), dvw,
|
|
1832
|
+
# "<f2f1>", type(f2f1), f2f1,
|
|
1833
|
+
# "",
|
|
1834
|
+
# sep="\n"
|
|
1835
|
+
# )
|
|
1836
|
+
|
|
1837
|
+
return f2f1
|
|
1838
|
+
|
|
1839
|
+
def estimate_electrons(self, inplace=False):
|
|
1840
|
+
r"""Estimate the electron parameters with a scalar temperature.
|
|
1841
|
+
|
|
1842
|
+
Assume temperature is the same as proton scalar temerature.
|
|
1843
|
+
"""
|
|
1844
|
+
|
|
1845
|
+
species = self.species
|
|
1846
|
+
|
|
1847
|
+
if "e" in species:
|
|
1848
|
+
msg = (
|
|
1849
|
+
r"Estimating electrons when there are e- in the data has been "
|
|
1850
|
+
r"disabled because I've screwed it up and estimated them as zero b/c "
|
|
1851
|
+
r"of various strange things. I need to disable `inplace` when `e` in "
|
|
1852
|
+
r"speces and do some ther things for this to work."
|
|
1853
|
+
)
|
|
1854
|
+
raise NotImplementedError(msg)
|
|
1855
|
+
|
|
1856
|
+
if "p" not in species and "p1" not in species:
|
|
1857
|
+
msg = (
|
|
1858
|
+
"Plasma must contain (core) protons to estimate electrons.\n"
|
|
1859
|
+
"Available species: {}".format(species)
|
|
1860
|
+
)
|
|
1861
|
+
raise ValueError(msg)
|
|
1862
|
+
elif "p" in species and "p1" in species:
|
|
1863
|
+
msg = (
|
|
1864
|
+
"Plasma cannot contain protons (p) and core protons (p1).\n"
|
|
1865
|
+
"Available species: {}".format(species)
|
|
1866
|
+
)
|
|
1867
|
+
raise ValueError(msg)
|
|
1868
|
+
elif "p" in species and "p1" not in species:
|
|
1869
|
+
tkw = "p"
|
|
1870
|
+
elif "p" not in species and "p1" in species:
|
|
1871
|
+
tkw = "p1"
|
|
1872
|
+
else:
|
|
1873
|
+
msg = "Unrecognized species: {}".format(species)
|
|
1874
|
+
raise ValueError(species)
|
|
1875
|
+
|
|
1876
|
+
qi = self.constants.charge_states.loc[list(species)]
|
|
1877
|
+
ni = self.number_density(*species)
|
|
1878
|
+
vi = self.velocity(*species)
|
|
1879
|
+
if isinstance(vi, vector.Vector):
|
|
1880
|
+
# Then we only have a single component proton plasma.
|
|
1881
|
+
qi = qi.loc[species[0]]
|
|
1882
|
+
vi = vi.cartesian
|
|
1883
|
+
niqi = ni.multiply(qi)
|
|
1884
|
+
ne = niqi
|
|
1885
|
+
niqivi = vi.multiply(niqi, axis=0)
|
|
1886
|
+
else:
|
|
1887
|
+
vi = pd.concat(
|
|
1888
|
+
vi.apply(lambda x: x.cartesian).to_dict(), axis=1, names="S", sort=True
|
|
1889
|
+
)
|
|
1890
|
+
niqi = ni.multiply(qi, axis=1, level="S")
|
|
1891
|
+
ne = niqi.sum(axis=1)
|
|
1892
|
+
# niqivi = vi.multiply(niqi, axis=1, level="S").sum(axis=1, level="C")
|
|
1893
|
+
niqivi = (
|
|
1894
|
+
vi.multiply(niqi, axis=1, level="S").T.groupby(level="C").sum().T
|
|
1895
|
+
) # sum(axis=1, level="C")
|
|
1896
|
+
|
|
1897
|
+
ve = niqivi.divide(ne, axis=0)
|
|
1898
|
+
|
|
1899
|
+
wp = self.w(tkw).loc[:, "scalar"]
|
|
1900
|
+
nrat = self.number_density(tkw).divide(ne, axis=0)
|
|
1901
|
+
mpme = self.constants.m_in_mp["e"] ** -1
|
|
1902
|
+
we = (nrat * mpme).multiply(wp.pow(2), axis=0).pipe(np.sqrt)
|
|
1903
|
+
we = pd.concat([we, we], axis=1, keys=["par", "per"], sort=True)
|
|
1904
|
+
|
|
1905
|
+
ne.name = ""
|
|
1906
|
+
electrons = pd.concat(
|
|
1907
|
+
[ne, ve, we], axis=1, keys=["n", "v", "w"], names=["M", "C"], sort=True
|
|
1908
|
+
)
|
|
1909
|
+
mask = ~ne.astype(bool)
|
|
1910
|
+
electrons = electrons.mask(mask, axis=0)
|
|
1911
|
+
|
|
1912
|
+
electrons = ions.Ion(electrons, "e")
|
|
1913
|
+
|
|
1914
|
+
if inplace:
|
|
1915
|
+
cols = electrons.data.columns
|
|
1916
|
+
cols = [x + ("e",) for x in cols.values]
|
|
1917
|
+
cols = pd.MultiIndex.from_tuples(cols, names=["M", "C", "S"])
|
|
1918
|
+
electrons.data.columns = cols
|
|
1919
|
+
|
|
1920
|
+
data = self.data
|
|
1921
|
+
if data.columns.intersection(electrons.data.columns).size:
|
|
1922
|
+
data.update(electrons.data)
|
|
1923
|
+
else:
|
|
1924
|
+
data = pd.concat([data, electrons.data], axis=1, sort=True)
|
|
1925
|
+
species = sorted(self.species + ("e",))
|
|
1926
|
+
self._set_species(*species)
|
|
1927
|
+
self.set_data(data)
|
|
1928
|
+
self._set_ions()
|
|
1929
|
+
|
|
1930
|
+
# print("<Module>",
|
|
1931
|
+
# "<species>: {}".format(species),
|
|
1932
|
+
# "<qi>", type(qi), qi,
|
|
1933
|
+
# "<ni>", type(ni), ni,
|
|
1934
|
+
# "<vi>", type(vi), vi,
|
|
1935
|
+
# "<wp>", type(wp), wp,
|
|
1936
|
+
# "<niqi>", type(niqi), niqi,
|
|
1937
|
+
# "<niqivi>", type(niqivi), niqivi,
|
|
1938
|
+
# "<ne>", type(ne), ne,
|
|
1939
|
+
# "<ve>", type(ve), ve,
|
|
1940
|
+
# "<we>", type(we), we,
|
|
1941
|
+
# "<electrons>", type(electrons), electrons, electrons.data,
|
|
1942
|
+
# "<plasma.species>: {}".format(self.species),
|
|
1943
|
+
# "<plasma.ions>", type(self.ions), self.ions,
|
|
1944
|
+
# "<plasma.data>", type(self.data), self.data.T,
|
|
1945
|
+
# "", sep="\n")
|
|
1946
|
+
|
|
1947
|
+
return electrons
|
|
1948
|
+
|
|
1949
|
+
def heat_flux(self, *species):
|
|
1950
|
+
r"""Calculate the parallel heat flux.
|
|
1951
|
+
|
|
1952
|
+
:math:`Q_\parallel = \rho (v^3 + \frac{3}{2}vw^2)`
|
|
1953
|
+
|
|
1954
|
+
where :math:`v` is each species' velocity in the Center-of-Mass frame and
|
|
1955
|
+
:math:`w` is each species parallel thermal speed.
|
|
1956
|
+
|
|
1957
|
+
Parameters
|
|
1958
|
+
----------
|
|
1959
|
+
species: list of strings
|
|
1960
|
+
The species to use. If a sum is indicated, take the sum
|
|
1961
|
+
of the input species.
|
|
1962
|
+
|
|
1963
|
+
Returns
|
|
1964
|
+
-------
|
|
1965
|
+
q: `pd.Series` or `pd.DataFrame`
|
|
1966
|
+
Dimensionality depends on species inputs.
|
|
1967
|
+
"""
|
|
1968
|
+
|
|
1969
|
+
slist = self._chk_species(*species)
|
|
1970
|
+
if len(slist) <= 1:
|
|
1971
|
+
raise ValueError("Must have >1 species to calculate heatflux.")
|
|
1972
|
+
|
|
1973
|
+
scom = "+".join(slist)
|
|
1974
|
+
rho = self.mass_density(*slist)
|
|
1975
|
+
dv = {s: self.dv(s, scom).project(self.b).par for s in slist}
|
|
1976
|
+
dv = pd.concat(dv, axis=1, names=["S"], sort=True)
|
|
1977
|
+
dv.columns.name = "S"
|
|
1978
|
+
w = self.data.w.par.loc[:, slist]
|
|
1979
|
+
|
|
1980
|
+
qa = dv.pow(3)
|
|
1981
|
+
qb = dv.multiply(w.pow(2), axis=1, level="S").multiply(3.0 / 2.0)
|
|
1982
|
+
|
|
1983
|
+
# print("<Module>",
|
|
1984
|
+
# "<species> {}".format(species),
|
|
1985
|
+
# "<rho>", type(rho), rho,
|
|
1986
|
+
# "<v>", type(v), v,
|
|
1987
|
+
# "<w>", type(w), w,
|
|
1988
|
+
# "<qa>", type(qa), qa,
|
|
1989
|
+
# "<qb>", type(qb), qb,
|
|
1990
|
+
# sep="\n")
|
|
1991
|
+
|
|
1992
|
+
qs = qa.add(qb, axis=1, level="S").multiply(rho, axis=0)
|
|
1993
|
+
if len(species) == 1:
|
|
1994
|
+
qs = qs.sum(axis=1)
|
|
1995
|
+
qs.name = "+".join(species)
|
|
1996
|
+
|
|
1997
|
+
# print("<qpar>", type(qs), qs,
|
|
1998
|
+
# sep="\n")
|
|
1999
|
+
|
|
2000
|
+
coeff = self.units.rho * (self.units.v**3.0) / self.units.qpar
|
|
2001
|
+
q = coeff * qs
|
|
2002
|
+
return q
|
|
2003
|
+
|
|
2004
|
+
def qpar(self, *species):
|
|
2005
|
+
r"""Shortcut to :py:meth:`heat_flux`."""
|
|
2006
|
+
return self.heat_flux(*species)
|
|
2007
|
+
|
|
2008
|
+
def build_alfvenic_turbulence(self, species, **kwargs):
|
|
2009
|
+
# raise NotImplementedError("Still working on module dev")
|
|
2010
|
+
r"""Create an Alfvenic turbulence instance.
|
|
2011
|
+
|
|
2012
|
+
Parameters
|
|
2013
|
+
----------
|
|
2014
|
+
species: str
|
|
2015
|
+
Species identifier. When no `,` present, use center-of-mass
|
|
2016
|
+
velocity as the velocity term. Alternatively, may contain up to
|
|
2017
|
+
one `,`. This is a unique `Plasma` case in which `s0+s1,s0+s1+s2`
|
|
2018
|
+
is a valid identifier. Here, the 2nd species is treated as the
|
|
2019
|
+
mass density passed to `AlfvenTurbulence` and used for converting
|
|
2020
|
+
magentic field in Alfven units.
|
|
2021
|
+
kwargs:
|
|
2022
|
+
Passed to `rolling` method in
|
|
2023
|
+
:py:class:`~solarwindpy.core.alfvenic_turbulence.AlfvenicTurbulence`
|
|
2024
|
+
to specify window size.
|
|
2025
|
+
"""
|
|
2026
|
+
species_ = species.split(",")
|
|
2027
|
+
|
|
2028
|
+
b = self.bfield.cartesian
|
|
2029
|
+
|
|
2030
|
+
if len(species_) == 1:
|
|
2031
|
+
# Don't hold onto `_chk_species` return because we need `velocity` and
|
|
2032
|
+
# `mass_density` to process center-of-mass species. (20190325)
|
|
2033
|
+
self._chk_species(species_[0])
|
|
2034
|
+
v = self.velocity(species)
|
|
2035
|
+
r = self.mass_density(species)
|
|
2036
|
+
|
|
2037
|
+
elif len(species_) == 2:
|
|
2038
|
+
slist0 = self._chk_species(species_[0])
|
|
2039
|
+
slist1 = self._chk_species(species_[1])
|
|
2040
|
+
|
|
2041
|
+
s0 = "+".join(slist0)
|
|
2042
|
+
s1 = "+".join(slist1)
|
|
2043
|
+
v = self.dv(s0, s1)
|
|
2044
|
+
r = self.mass_density(s1)
|
|
2045
|
+
|
|
2046
|
+
else:
|
|
2047
|
+
msg = "`species` can only contain at most 1 comma\nspecies: %s"
|
|
2048
|
+
raise ValueError(msg % species)
|
|
2049
|
+
|
|
2050
|
+
v = v.cartesian
|
|
2051
|
+
|
|
2052
|
+
turb = alf_turb.AlfvenicTurbulence(v, b, r, species, **kwargs)
|
|
2053
|
+
|
|
2054
|
+
return turb
|
|
2055
|
+
|
|
2056
|
+
def S(self, *species):
|
|
2057
|
+
r"""Shortcut to :py:meth:`specific_entropy`."""
|
|
2058
|
+
return self.specific_entropy(*species)
|
|
2059
|
+
|
|
2060
|
+
def specific_entropy(self, *species):
|
|
2061
|
+
r"""Calculate the specific entropy following [1] as.
|
|
2062
|
+
|
|
2063
|
+
:math:`p_\mathrm{th} \rho^{-\gamma}`
|
|
2064
|
+
|
|
2065
|
+
where :math:`gamma=5/3`, :math:`p_\mathrm{th}` is the thermal presure,
|
|
2066
|
+
and :math:`rho` is the mass density.
|
|
2067
|
+
|
|
2068
|
+
Parameters
|
|
2069
|
+
----------
|
|
2070
|
+
species: str or list-like of str
|
|
2071
|
+
Comma separated strings ("a,p1") are invalid.
|
|
2072
|
+
Comma separated lists ("a", "p1") are valid.
|
|
2073
|
+
Total effective species ("a+p1") are valid and use
|
|
2074
|
+
|
|
2075
|
+
:math:`p_\mathrm{th} = \sum_s p_{\mathrm{th},s}`
|
|
2076
|
+
:math:`\rho = \sum_s \rho_s`.
|
|
2077
|
+
|
|
2078
|
+
References
|
|
2079
|
+
----------
|
|
2080
|
+
[1] Siscoe, G. L. (1983). Solar System Magnetohydrodynamics (pp.
|
|
2081
|
+
11–100). <https://doi.org/10.1007/978-94-009-7194-3_2>.
|
|
2082
|
+
"""
|
|
2083
|
+
multi_species = len(species) > 1
|
|
2084
|
+
gamma = self.constants.polytropic_index["scalar"]
|
|
2085
|
+
|
|
2086
|
+
pth = self.pth(*species).xs(
|
|
2087
|
+
"scalar", axis=1, level="C" if multi_species else None
|
|
2088
|
+
)
|
|
2089
|
+
rho = self.rho(*species)
|
|
2090
|
+
|
|
2091
|
+
pth *= self.units.pth
|
|
2092
|
+
rho *= self.units.rho
|
|
2093
|
+
|
|
2094
|
+
out = pth.multiply(
|
|
2095
|
+
rho.pow(-gamma),
|
|
2096
|
+
axis=1 if multi_species else 0,
|
|
2097
|
+
level="S" if multi_species else None,
|
|
2098
|
+
)
|
|
2099
|
+
out /= self.units.specific_entropy
|
|
2100
|
+
out.name = "S"
|
|
2101
|
+
|
|
2102
|
+
return out
|
|
2103
|
+
|
|
2104
|
+
def kinetic_energy_flux(self, *species):
|
|
2105
|
+
r"""Calculate the plasma kinetic energy flux.
|
|
2106
|
+
|
|
2107
|
+
Parameters
|
|
2108
|
+
----------
|
|
2109
|
+
species: str
|
|
2110
|
+
Each species is a string. If only one string is passed, it can
|
|
2111
|
+
contain "+". If this is the case, the species are summed over and
|
|
2112
|
+
a pd.Series is returned. Otherwise, the individual quantities are
|
|
2113
|
+
returned as a pd.DataFrame.
|
|
2114
|
+
|
|
2115
|
+
Returns
|
|
2116
|
+
-------
|
|
2117
|
+
rho: pd.Series or pd.DataFrame
|
|
2118
|
+
See Parameters for more info.
|
|
2119
|
+
"""
|
|
2120
|
+
slist = self._chk_species(*species)
|
|
2121
|
+
|
|
2122
|
+
w = {s: self.ions.loc[s].kinetic_energy_flux for s in slist}
|
|
2123
|
+
w = pd.concat(w, axis=1, names=["S"], sort=True)
|
|
2124
|
+
|
|
2125
|
+
if len(species) == 1:
|
|
2126
|
+
w = w.sum(axis=1)
|
|
2127
|
+
w.name = species[0]
|
|
2128
|
+
|
|
2129
|
+
return w
|
|
2130
|
+
|
|
2131
|
+
def Wk(self, *species):
|
|
2132
|
+
r"""Shortcut to :py:meth:`~kinetic_energy_flux`."""
|
|
2133
|
+
return self.kinetic_energy_flux(*species)
|