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.

Files changed (412) hide show
  1. plans/.velocity/metrics.json +96 -0
  2. plans/0-overview-template.md +268 -0
  3. plans/N-phase-template.md +106 -0
  4. plans/PLAN_AUDIT_SUMMARY.md +173 -0
  5. plans/TEMPLATE-USAGE-GUIDE.md +198 -0
  6. plans/__init__.py +1 -0
  7. plans/abandoned/compaction-agent-system/0-Overview.md +123 -0
  8. plans/abandoned/compaction-agent-system/agents-index-update-plan.md +109 -0
  9. plans/abandoned/compaction-agent-system/compacted_state.md +85 -0
  10. plans/abandoned/compaction-agent-system/implementation-plan.md +107 -0
  11. plans/abandoned/compaction-agent-system/system-validation-report.md +159 -0
  12. plans/abandoned/compaction-agent-system/usage-guide.md +210 -0
  13. plans/abandoned/hook-system-enhancement/0-Overview.md +214 -0
  14. plans/abandoned/hook-system-enhancement/1-Phase1-Core-Infrastructure.md +313 -0
  15. plans/abandoned/hook-system-enhancement/2-Phase2-Intelligent-Testing.md +385 -0
  16. plans/abandoned/hook-system-enhancement/3-Phase3-Physics-Validation.md +444 -0
  17. plans/abandoned/hook-system-enhancement/4-Phase4-Performance-Monitoring.md +458 -0
  18. plans/abandoned/hook-system-enhancement/5-Phase5-Developer-Experience.md +532 -0
  19. plans/abandoned/hook-system-enhancement/6-Implementation-Timeline.md +274 -0
  20. plans/abandoned/hook-system-enhancement/7-Risk-Management.md +376 -0
  21. plans/abandoned/hook-system-enhancement/8-Testing-Strategy.md +579 -0
  22. plans/abandoned/readthedocs-automation/0-Overview.md +247 -0
  23. plans/abandoned/readthedocs-automation/1-Emergency-Documentation-Fixes.md +270 -0
  24. plans/abandoned/readthedocs-automation/2-Template-System-Enhancement.md +811 -0
  25. plans/abandoned/readthedocs-automation/3-Quality-Audit-ReadTheDocs-Integration.md +844 -0
  26. plans/abandoned/readthedocs-automation/4-Plan-Consolidation-Cleanup.md +632 -0
  27. plans/abandoned/readthedocs-automation/9-Closeout.md +207 -0
  28. plans/abandoned/readthedocs-automation/ABANDONMENT_REASON.md +72 -0
  29. plans/cicd-architecture-redesign/0-Overview.md +193 -0
  30. plans/cicd-architecture-redesign/1-Workflow-Creation.md +103 -0
  31. plans/cicd-architecture-redesign/2-Version-Detection.md +123 -0
  32. plans/cicd-architecture-redesign/3-Deployment-Gates.md +169 -0
  33. plans/cicd-architecture-redesign/4-RC-Testing.md +194 -0
  34. plans/cicd-architecture-redesign/5-TestPyPI-Validation.md +264 -0
  35. plans/cicd-architecture-redesign/6-Production-Release.md +263 -0
  36. plans/cicd-architecture-redesign/7-Cleanup.md +243 -0
  37. plans/cicd-architecture-redesign/8-Documentation.md +285 -0
  38. plans/cicd-architecture-redesign/Closeout.md +225 -0
  39. plans/closeout-template.md +259 -0
  40. plans/completed/circular-import-audit/0-Overview.md +152 -0
  41. plans/completed/circular-import-audit/1-Static-Dependency-Analysis.md +62 -0
  42. plans/completed/circular-import-audit/2-Dynamic-Import-Testing.md +56 -0
  43. plans/completed/circular-import-audit/3-Performance-Impact-Assessment.md +56 -0
  44. plans/completed/circular-import-audit/4-Issue-Remediation.md +78 -0
  45. plans/completed/circular-import-audit/5-Preventive-Infrastructure.md +89 -0
  46. plans/completed/claude-settings-ecosystem-alignment/0-Overview.md +162 -0
  47. plans/completed/claude-settings-ecosystem-alignment/1-Security-Foundation.md +148 -0
  48. plans/completed/claude-settings-ecosystem-alignment/2-Hook-Integration.md +158 -0
  49. plans/completed/claude-settings-ecosystem-alignment/3-Agent-System-Integration.md +177 -0
  50. plans/completed/claude-settings-ecosystem-alignment/4-Enhanced-Workflow-Automation.md +159 -0
  51. plans/completed/claude-settings-ecosystem-alignment/5-Validation-Monitoring.md +181 -0
  52. plans/completed/claude-settings-ecosystem-alignment/compacted_session_state.md +290 -0
  53. plans/completed/combined_plan_with_checklist_documentation/1-Overview-and-Goals.md +51 -0
  54. plans/completed/combined_plan_with_checklist_documentation/2-Toolchain-and-Hosting.md +69 -0
  55. plans/completed/combined_plan_with_checklist_documentation/3-Repository-Structure.md +61 -0
  56. plans/completed/combined_plan_with_checklist_documentation/4-Configuration-and-Standards.md +70 -0
  57. plans/completed/combined_plan_with_checklist_documentation/5-Documentation-Content.md +62 -0
  58. plans/completed/combined_plan_with_checklist_documentation/6-CI-CD-and-Validation.md +58 -0
  59. plans/completed/combined_plan_with_checklist_documentation/7-Maintenance.md +55 -0
  60. plans/completed/combined_test_plan_with_checklist_fitfunctions/0-Overview.md +135 -0
  61. plans/completed/combined_test_plan_with_checklist_fitfunctions/1-Common-fixtures.md +59 -0
  62. plans/completed/combined_test_plan_with_checklist_fitfunctions/10-power_laws.md +56 -0
  63. plans/completed/combined_test_plan_with_checklist_fitfunctions/2-core.py-FitFunction.md +118 -0
  64. plans/completed/combined_test_plan_with_checklist_fitfunctions/3-gaussians.py-Gaussian-GaussianNormalized-GaussianLn.md +69 -0
  65. plans/completed/combined_test_plan_with_checklist_fitfunctions/4-trend_fits.py-TrendFit.md +99 -0
  66. plans/completed/combined_test_plan_with_checklist_fitfunctions/5-plots.py-FFPlot.md +98 -0
  67. plans/completed/combined_test_plan_with_checklist_fitfunctions/6-tex_info.py-TeXinfo.md +79 -0
  68. plans/completed/combined_test_plan_with_checklist_fitfunctions/7-Justification.md +49 -0
  69. plans/completed/combined_test_plan_with_checklist_fitfunctions/8-exponentials.md +64 -0
  70. plans/completed/combined_test_plan_with_checklist_fitfunctions/9-lines.md +58 -0
  71. plans/completed/combined_test_plan_with_checklist_plotting/0-Overview.md +142 -0
  72. plans/completed/combined_test_plan_with_checklist_plotting/1-base.py.md +90 -0
  73. plans/completed/combined_test_plan_with_checklist_plotting/10-labels-special.py.md +102 -0
  74. plans/completed/combined_test_plan_with_checklist_plotting/11-labels-chemistry.py.md +212 -0
  75. plans/completed/combined_test_plan_with_checklist_plotting/12-labels-composition.py.md +242 -0
  76. plans/completed/combined_test_plan_with_checklist_plotting/13-labels-datetime.py.md +247 -0
  77. plans/completed/combined_test_plan_with_checklist_plotting/14-labels-elemental_abundance.py.md +274 -0
  78. plans/completed/combined_test_plan_with_checklist_plotting/15-visual-validation.md +256 -0
  79. plans/completed/combined_test_plan_with_checklist_plotting/16-integration-testing.md +266 -0
  80. plans/completed/combined_test_plan_with_checklist_plotting/17-performance-benchmarks.md +267 -0
  81. plans/completed/combined_test_plan_with_checklist_plotting/18-Fixtures-and-Utilities.md +86 -0
  82. plans/completed/combined_test_plan_with_checklist_plotting/2-agg_plot.py.md +90 -0
  83. plans/completed/combined_test_plan_with_checklist_plotting/3-histograms.py.md +201 -0
  84. plans/completed/combined_test_plan_with_checklist_plotting/4-scatter.py.md +167 -0
  85. plans/completed/combined_test_plan_with_checklist_plotting/5-spiral.py.md +216 -0
  86. plans/completed/combined_test_plan_with_checklist_plotting/6-orbits.py.md +108 -0
  87. plans/completed/combined_test_plan_with_checklist_plotting/7-tools.py.md +86 -0
  88. plans/completed/combined_test_plan_with_checklist_plotting/8-select_data_from_figure.py.md +97 -0
  89. plans/completed/combined_test_plan_with_checklist_plotting/9-labels-base.py.md +88 -0
  90. plans/completed/combined_test_plan_with_checklist_solar_activity/.gitkeep +0 -0
  91. plans/completed/combined_test_plan_with_checklist_solar_activity/0-Overview.md +170 -0
  92. plans/completed/combined_test_plan_with_checklist_solar_activity/1-Package-Entry-Point-__init__.py.md +121 -0
  93. plans/completed/combined_test_plan_with_checklist_solar_activity/2-Core-Base-Classes-base.py.md +142 -0
  94. plans/completed/combined_test_plan_with_checklist_solar_activity/3-Plotting-Helpers-plots.py.md +123 -0
  95. plans/completed/combined_test_plan_with_checklist_solar_activity/4-LISIRD-Sub-package.md +119 -0
  96. plans/completed/combined_test_plan_with_checklist_solar_activity/5-Extrema-Calculator.md +103 -0
  97. plans/completed/combined_test_plan_with_checklist_solar_activity/6-Sunspot-Number-Sub-package.md +163 -0
  98. plans/completed/combined_test_plan_with_checklist_solar_activity/7-Sunspot-Number-Init.py.md +217 -0
  99. plans/completed/combined_test_plan_with_checklist_solar_activity/compacted_state.md +52 -0
  100. plans/completed/compaction-agent-modernization/0-Overview.md +156 -0
  101. plans/completed/compaction-agent-modernization/1-Architecture-Audit-Gap-Analysis.md +132 -0
  102. plans/completed/compaction-agent-modernization/2-Token-Baseline-Recalibration.md +153 -0
  103. plans/completed/compaction-agent-modernization/3-Agent-Reference-Updates.md +184 -0
  104. plans/completed/compaction-agent-modernization/4-Compression-Algorithm-Modernization.md +238 -0
  105. plans/completed/compaction-agent-modernization/5-Workflow-Integration-Streamlining.md +252 -0
  106. plans/completed/compaction-agent-modernization/6-Template-Structure-Optimization.md +240 -0
  107. plans/completed/compaction-agent-modernization/7-Integration-Testing-Validation.md +292 -0
  108. plans/completed/compaction-hook-enhancement/0-Overview.md +150 -0
  109. plans/completed/compaction-hook-enhancement/1-Token-Estimation-Enhancement.md +179 -0
  110. plans/completed/compaction-hook-enhancement/2-Compression-Intelligence.md +294 -0
  111. plans/completed/compaction-hook-enhancement/3-Git-Integration-Metadata.md +310 -0
  112. plans/completed/compaction-hook-enhancement/4-Session-Continuity-Features.md +358 -0
  113. plans/completed/compaction-hook-enhancement/5-Testing-Strategy.md +404 -0
  114. plans/completed/compaction-hook-enhancement/6-Integration-Roadmap.md +319 -0
  115. plans/completed/compaction-hook-enhancement/compacted_state.md +142 -0
  116. plans/completed/docstring-audit-enhancement/0-Overview.md +274 -0
  117. plans/completed/docstring-audit-enhancement/1-Infrastructure-Setup-and-Validation-Tools.md +206 -0
  118. plans/completed/docstring-audit-enhancement/2-Core-Physics-Modules-Enhancement.md +237 -0
  119. plans/completed/docstring-audit-enhancement/3-Fitfunctions-Mathematical-Modules-Enhancement.md +188 -0
  120. plans/completed/docstring-audit-enhancement/4-Plotting-Visualization-Modules-Enhancement.md +243 -0
  121. plans/completed/docstring-audit-enhancement/5-Specialized-Modules-Enhancement.md +216 -0
  122. plans/completed/docstring-audit-enhancement/6-Validation-and-Integration.md +216 -0
  123. plans/completed/fitfunctions-testing-implementation/0-Overview.md +130 -0
  124. plans/completed/fitfunctions-testing-implementation/1-Test-Infrastructure-Setup.md +79 -0
  125. plans/completed/fitfunctions-testing-implementation/2-Common-Fixtures-Test-Utilities.md +104 -0
  126. plans/completed/fitfunctions-testing-implementation/3-Core-FitFunction-Testing.md +168 -0
  127. plans/completed/fitfunctions-testing-implementation/4-Specialized-Function-Classes.md +210 -0
  128. plans/completed/fitfunctions-testing-implementation/5-Advanced-Classes-Testing.md +214 -0
  129. plans/completed/fitfunctions-testing-implementation/6-Plotting-Integration-Testing.md +231 -0
  130. plans/completed/fitfunctions-testing-implementation/7-Extended-Coverage-BONUS.md +184 -0
  131. plans/completed/numpy-docstring-conversion-plan/numpy-docstring-conversion-plan.md +118 -0
  132. plans/completed/pr-review-remediation/0-Overview.md +138 -0
  133. plans/completed/pr-review-remediation/1-Critical-Safety-Improvements.md +179 -0
  134. plans/completed/pr-review-remediation/2-Smart-Timeouts-Validation.md +399 -0
  135. plans/completed/pr-review-remediation/3-Enhanced-GitHub-Integration.md +258 -0
  136. plans/completed/pr-review-remediation/compacted_state.md +66 -0
  137. plans/completed/python-310-migration/0-Overview.md +390 -0
  138. plans/completed/python-310-migration/1-Planning-Setup.md +164 -0
  139. plans/completed/python-310-migration/2-Implementation.md +256 -0
  140. plans/completed/python-310-migration/3-Testing-Validation.md +335 -0
  141. plans/completed/python-310-migration/4-Documentation-Release.md +274 -0
  142. plans/completed/python-310-migration/5-Closeout.md +252 -0
  143. plans/completed/requirements-management-consolidation/0-Overview.md +118 -0
  144. plans/completed/requirements-management-consolidation/1-Documentation-Validation-Environment-Setup.md +116 -0
  145. plans/completed/requirements-management-consolidation/2-Requirements-Consolidation.md +161 -0
  146. plans/completed/requirements-management-consolidation/3-Workflow-Automation-Final-Integration.md +196 -0
  147. plans/completed/single-ecosystem-plan-implementation/0-Overview.md +83 -0
  148. plans/completed/single-ecosystem-plan-implementation/1-Plan-Preservation-Session-Management.md +38 -0
  149. plans/completed/single-ecosystem-plan-implementation/2-File-Structure-Optimization.md +43 -0
  150. plans/completed/single-ecosystem-plan-implementation/3-Plan-Migration-Archive-Setup.md +82 -0
  151. plans/completed/single-ecosystem-plan-implementation/4-Agent-System-Transformation.md +108 -0
  152. plans/completed/single-ecosystem-plan-implementation/5-Template-System-Enhancement.md +131 -0
  153. plans/completed/single-ecosystem-plan-implementation/6-Final-Validation-Testing.md +120 -0
  154. plans/completed/test-directory-consolidation/0-Overview.md +51 -0
  155. plans/completed/test-directory-consolidation/1-Structure-Preparation.md +82 -0
  156. plans/completed/test-directory-consolidation/2-File-Migration.md +100 -0
  157. plans/completed/test-directory-consolidation/3-Import-Transformation.md +117 -0
  158. plans/completed/test-directory-consolidation/4-Configuration-Consolidation.md +140 -0
  159. plans/completed/test-directory-consolidation/5-Validation.md +152 -0
  160. plans/completed/test-directory-consolidation/6-Cleanup.md +156 -0
  161. plans/completed/test-planning-agents-architecture/0-Overview.md +79 -0
  162. plans/completed/test-planning-agents-architecture/1-Branch-Isolation-Testing.md +49 -0
  163. plans/completed/test-planning-agents-architecture/2-Cross-Branch-Coordination.md +51 -0
  164. plans/completed/test-planning-agents-architecture/3-Merge-Workflow-Testing.md +48 -0
  165. plans/deployment-semver-pypi-rtd/0-Overview.md +463 -0
  166. plans/deployment-semver-pypi-rtd/1-Semantic-Versioning-Foundation.md +136 -0
  167. plans/deployment-semver-pypi-rtd/2-PyPI-Deployment-Infrastructure.md +168 -0
  168. plans/deployment-semver-pypi-rtd/3-Release-Automation.md +214 -0
  169. plans/deployment-semver-pypi-rtd/4-Plan-Closeout.md +543 -0
  170. plans/deployment-semver-pypi-rtd/compacted_session_state.md +172 -0
  171. plans/deployment-semver-pypi-rtd/compacted_state.md +131 -0
  172. plans/documentation-code-audit/0-Overview.md +393 -0
  173. plans/documentation-code-audit/1-Discovery-Inventory.md +183 -0
  174. plans/documentation-code-audit/2-Execution-Environment-Setup.md +263 -0
  175. plans/documentation-code-audit/3-Systematic-Validation.md +322 -0
  176. plans/documentation-code-audit/4-Code-Example-Remediation.md +358 -0
  177. plans/documentation-code-audit/5-Physics-MultiIndex-Compliance.md +464 -0
  178. plans/documentation-code-audit/6-Doctest-Integration.md +523 -0
  179. plans/documentation-code-audit/7-Reporting-Documentation.md +498 -0
  180. plans/documentation-code-audit/8-Closeout.md +456 -0
  181. plans/documentation-rebuild-session/compacted_state.md +109 -0
  182. plans/documentation-rendering-fixes/0-Overview.md +104 -0
  183. plans/documentation-rendering-fixes/1-Sphinx-Build-Diagnostics-Warning-Audit.md +101 -0
  184. plans/documentation-rendering-fixes/2-Configuration-Infrastructure-Fixes.md +113 -0
  185. plans/documentation-rendering-fixes/3-Docstring-Syntax-Audit-Repair.md +131 -0
  186. plans/documentation-rendering-fixes/4-HTML-Page-Rendering-Verification.md +113 -0
  187. plans/documentation-rendering-fixes/5-Advanced-Documentation-Quality-Assurance.md +119 -0
  188. plans/documentation-rendering-fixes/6-Documentation-Build-Optimization-Testing.md +129 -0
  189. plans/documentation-rendering-fixes/compacted_state.md +132 -0
  190. plans/documentation-template-fix/0-Overview.md +197 -0
  191. plans/documentation-template-fix/1-Template-System-Analysis.md +269 -0
  192. plans/documentation-template-fix/2-Template-Modification.md +609 -0
  193. plans/documentation-template-fix/3-Build-System-Integration.md +766 -0
  194. plans/documentation-template-fix/4-Testing-Validation.md +1399 -0
  195. plans/documentation-template-fix/5-Documentation-Training.md +602 -0
  196. plans/documentation-workflow-fix/0-Overview.md +222 -0
  197. plans/documentation-workflow-fix/1-Immediate-Fixes.md +238 -0
  198. plans/documentation-workflow-fix/2-Configuration-Setup.md +298 -0
  199. plans/documentation-workflow-fix/3-Pre-commit-Integration.md +382 -0
  200. plans/documentation-workflow-fix/4-Workflow-Improvements.md +446 -0
  201. plans/documentation-workflow-fix/5-Documentation-and-Training.md +527 -0
  202. plans/duplicate-object-warnings-fix-plan.md +130 -0
  203. plans/github-issues-migration/0-Overview.md +510 -0
  204. plans/github-issues-migration/1-Foundation-Label-System.md +180 -0
  205. plans/github-issues-migration/2-Migration-Tool-Rewrite.md +235 -0
  206. plans/github-issues-migration/3-CLI-Integration-Automation.md +169 -0
  207. plans/github-issues-migration/4-Validated-Migration.md +252 -0
  208. plans/github-issues-migration/5-Documentation-Training.md +171 -0
  209. plans/github-issues-migration/6-Closeout.md +179 -0
  210. plans/github-workflows-repair/repair-plan.md +299 -0
  211. plans/issues_from_plans.py +342 -0
  212. plans/pr-270-doc-validation-fixes/0-Overview.md +354 -0
  213. plans/pr-270-doc-validation-fixes/1-Critical-PR-Fixes.md +117 -0
  214. plans/pr-270-doc-validation-fixes/2-Framework-Right-Sizing.md +129 -0
  215. plans/pr-270-doc-validation-fixes/3-Sustainable-Documentation.md +126 -0
  216. plans/pr-270-doc-validation-fixes/4-Closeout-Migration.md +143 -0
  217. plans/pr-270-doc-validation-fixes/PLAN_COMPLETED.md +149 -0
  218. plans/python-310-migration/0-Overview.md +390 -0
  219. plans/python-310-migration/1-Planning-Setup.md +164 -0
  220. plans/python-310-migration/2-Implementation.md +256 -0
  221. plans/python-310-migration/3-Testing-Validation.md +335 -0
  222. plans/python-310-migration/4-Documentation-Release.md +274 -0
  223. plans/python-310-migration/5-Closeout.md +252 -0
  224. plans/readthedocs-simplified/0-Overview.md +243 -0
  225. plans/readthedocs-simplified/1-Immediate-Fixes.md +216 -0
  226. plans/readthedocs-simplified/2-Template-Simplification.md +278 -0
  227. plans/readthedocs-simplified/3-ReadTheDocs-Setup.md +298 -0
  228. plans/readthedocs-simplified/4-Testing-Validation.md +328 -0
  229. plans/readthedocs-simplified/5-Closeout.md +231 -0
  230. plans/readthedocs-simplified/compacted_state.md +127 -0
  231. plans/session-compaction-2025-08-12/compacted_state.md +114 -0
  232. plans/session-compaction-2025-08-13/compacted_state.md +145 -0
  233. plans/session-continuity-protocol/0-Overview.md +35 -0
  234. plans/session-continuity-protocol/1-Core-Principles-Framework.md +40 -0
  235. plans/session-continuity-protocol/2-Pre-Session-Validation-System.md +79 -0
  236. plans/session-continuity-protocol/3-Context-Switching-Prevention.md +87 -0
  237. plans/session-continuity-protocol/4-Progress-Tracking-Recovery.md +100 -0
  238. plans/sphinx-warnings-analysis.md +222 -0
  239. plans/systemprompt-optimization/0-Overview.md +447 -0
  240. plans/systemprompt-optimization/1-Deploy-SystemPrompt.md +114 -0
  241. plans/systemprompt-optimization/2-Documentation-Alignment.md +198 -0
  242. plans/systemprompt-optimization/3-Monitoring-Infrastructure.md +396 -0
  243. plans/systemprompt-optimization/4-Implementation-Script.md +450 -0
  244. plans/systemprompt-optimization/9-Closeout.md +165 -0
  245. plans/systemprompt-optimization/compacted_state.md +143 -0
  246. plans/template-value-propositions/0-Overview.md +357 -0
  247. plans/template-value-propositions/1-Value-Proposition-Framework-Design.md +144 -0
  248. plans/template-value-propositions/2-Plan-Template-Enhancement.md +178 -0
  249. plans/template-value-propositions/3-Value-Generator-Hook-Implementation.md +291 -0
  250. plans/template-value-propositions/4-Value-Validator-Hook-Implementation.md +274 -0
  251. plans/template-value-propositions/5-Documentation-Agent-Updates.md +219 -0
  252. plans/template-value-propositions/6-Integration-Testing-Validation.md +247 -0
  253. plans/tests-audit/0-Overview.md +410 -0
  254. plans/tests-audit/1-Discovery-Inventory.md +170 -0
  255. plans/tests-audit/2-Physics-Validation-Audit.md +195 -0
  256. plans/tests-audit/3-Architecture-Compliance.md +195 -0
  257. plans/tests-audit/4-Numerical-Stability-Analysis.md +203 -0
  258. plans/tests-audit/5-Documentation-Enhancement.md +220 -0
  259. plans/tests-audit/6-Audit-Deliverables.md +220 -0
  260. plans/tests-audit/7-Closeout.md +252 -0
  261. plans/tests-audit/artifacts/ARCHITECTURE_COMPLIANCE_REPORT.md +315 -0
  262. plans/tests-audit/artifacts/ARCHITECTURE_RECOMMENDATIONS.md +943 -0
  263. plans/tests-audit/artifacts/COMPREHENSIVE_AUDIT_REPORT.md +356 -0
  264. plans/tests-audit/artifacts/CONTRIBUTING_ENHANCED_TEMPLATE.md +419 -0
  265. plans/tests-audit/artifacts/COVERAGE_GAP_ANALYSIS.md +152 -0
  266. plans/tests-audit/artifacts/DOCUMENTATION_ENHANCEMENT_REPORT.md +502 -0
  267. plans/tests-audit/artifacts/EXECUTIVE_AUDIT_SUMMARY.md +129 -0
  268. plans/tests-audit/artifacts/IMPLEMENTATION_ROADMAP.md +647 -0
  269. plans/tests-audit/artifacts/NUMERICAL_RECOMMENDATIONS.md +739 -0
  270. plans/tests-audit/artifacts/NUMERICAL_STABILITY_GUIDE_TEMPLATE.rst +451 -0
  271. plans/tests-audit/artifacts/NUMERICAL_STABILITY_REPORT.md +301 -0
  272. plans/tests-audit/artifacts/PHASE_3_SUMMARY.md +280 -0
  273. plans/tests-audit/artifacts/PHASE_4_SUMMARY.md +229 -0
  274. plans/tests-audit/artifacts/PHASE_5_SUMMARY.md +292 -0
  275. plans/tests-audit/artifacts/PHASE_6_CLOSEOUT.md +278 -0
  276. plans/tests-audit/artifacts/PHYSICS_GUIDE_TEMPLATE.rst +268 -0
  277. plans/tests-audit/artifacts/PHYSICS_VALIDATION_REPORT.md +235 -0
  278. plans/tests-audit/artifacts/TECHNICAL_DELIVERABLES_PACKAGE.md +2502 -0
  279. plans/tests-audit/artifacts/TEST_INVENTORY.csv +1204 -0
  280. plans/tests-audit/artifacts/TEST_INVENTORY.md +135 -0
  281. plans/tests-audit/artifacts/test_discovery_analysis.py +231 -0
  282. plans/tests-audit/artifacts/test_parser.py +395 -0
  283. solarwindpy/README.md +3 -0
  284. solarwindpy/Untitled.ipynb +54 -0
  285. solarwindpy/__init__.py +74 -0
  286. solarwindpy/core/__init__.py +23 -0
  287. solarwindpy/core/alfvenic_turbulence.py +804 -0
  288. solarwindpy/core/base.py +267 -0
  289. solarwindpy/core/ions.py +309 -0
  290. solarwindpy/core/plasma.py +2133 -0
  291. solarwindpy/core/spacecraft.py +256 -0
  292. solarwindpy/core/tensor.py +90 -0
  293. solarwindpy/core/units_constants.py +199 -0
  294. solarwindpy/core/vector.py +328 -0
  295. solarwindpy/fitfunctions/__init__.py +20 -0
  296. solarwindpy/fitfunctions/core.py +734 -0
  297. solarwindpy/fitfunctions/exponentials.py +188 -0
  298. solarwindpy/fitfunctions/gaussians.py +264 -0
  299. solarwindpy/fitfunctions/lines.py +116 -0
  300. solarwindpy/fitfunctions/moyal.py +71 -0
  301. solarwindpy/fitfunctions/plots.py +751 -0
  302. solarwindpy/fitfunctions/power_laws.py +209 -0
  303. solarwindpy/fitfunctions/tex_info.py +568 -0
  304. solarwindpy/fitfunctions/trend_fits.py +482 -0
  305. solarwindpy/instabilities/__init__.py +16 -0
  306. solarwindpy/instabilities/beta_ani.py +82 -0
  307. solarwindpy/instabilities/verscharen2016.py +631 -0
  308. solarwindpy/plotting/__init__.py +33 -0
  309. solarwindpy/plotting/agg_plot.py +489 -0
  310. solarwindpy/plotting/base.py +465 -0
  311. solarwindpy/plotting/hist1d.py +405 -0
  312. solarwindpy/plotting/hist2d.py +1035 -0
  313. solarwindpy/plotting/histograms.py +1845 -0
  314. solarwindpy/plotting/labels/__init__.py +104 -0
  315. solarwindpy/plotting/labels/base.py +686 -0
  316. solarwindpy/plotting/labels/chemistry.py +19 -0
  317. solarwindpy/plotting/labels/composition.py +100 -0
  318. solarwindpy/plotting/labels/datetime.py +235 -0
  319. solarwindpy/plotting/labels/elemental_abundance.py +73 -0
  320. solarwindpy/plotting/labels/special.py +794 -0
  321. solarwindpy/plotting/orbits.py +515 -0
  322. solarwindpy/plotting/scatter.py +99 -0
  323. solarwindpy/plotting/select_data_from_figure.py +329 -0
  324. solarwindpy/plotting/spiral.py +980 -0
  325. solarwindpy/plotting/tools.py +434 -0
  326. solarwindpy/scripts/__init__.py +1 -0
  327. solarwindpy/scripts/logs/.gitignore +1 -0
  328. solarwindpy/solar_activity/__init__.py +53 -0
  329. solarwindpy/solar_activity/base.py +605 -0
  330. solarwindpy/solar_activity/lisird/__init__.py +3 -0
  331. solarwindpy/solar_activity/lisird/extrema_calculator.py +394 -0
  332. solarwindpy/solar_activity/lisird/lisird.py +319 -0
  333. solarwindpy/solar_activity/plots.py +116 -0
  334. solarwindpy/solar_activity/sunspot_number/.DS_Store +0 -0
  335. solarwindpy/solar_activity/sunspot_number/__init__.py +3 -0
  336. solarwindpy/solar_activity/sunspot_number/sidc.py +556 -0
  337. solarwindpy/solar_activity/sunspot_number/ssn_extrema.csv +72 -0
  338. solarwindpy/solar_activity/sunspot_number/ssn_extrema.csv.silso +72 -0
  339. solarwindpy/tools/__init__.py +162 -0
  340. solarwindpy-0.1.1.dist-info/METADATA +181 -0
  341. solarwindpy-0.1.1.dist-info/RECORD +409 -0
  342. {solarwindpy-0.0.1.dev0.dist-info → solarwindpy-0.1.1.dist-info}/WHEEL +1 -1
  343. solarwindpy-0.1.1.dist-info/licenses/LICENSE.rst +32 -0
  344. solarwindpy-0.1.1.dist-info/top_level.txt +3 -0
  345. tests/__init__.py +1 -0
  346. tests/conftest.py +10 -0
  347. tests/core/__init__.py +1 -0
  348. tests/core/test_alfvenic_turbulence.py +544 -0
  349. tests/core/test_base.py +112 -0
  350. tests/core/test_base_head_tail.py +29 -0
  351. tests/core/test_base_mi_tuples.py +11 -0
  352. tests/core/test_core_verify_datetimeindex.py +32 -0
  353. tests/core/test_ions.py +325 -0
  354. tests/core/test_plasma.py +2581 -0
  355. tests/core/test_plasma_io.py +12 -0
  356. tests/core/test_quantities.py +507 -0
  357. tests/core/test_spacecraft.py +210 -0
  358. tests/core/test_units_constants.py +22 -0
  359. tests/data/epoch.csv +4 -0
  360. tests/data/plasma.csv +4 -0
  361. tests/data/spacecraft.csv +4 -0
  362. tests/fitfunctions/conftest.py +60 -0
  363. tests/fitfunctions/test_core.py +193 -0
  364. tests/fitfunctions/test_exponentials.py +342 -0
  365. tests/fitfunctions/test_gaussians.py +142 -0
  366. tests/fitfunctions/test_lines.py +349 -0
  367. tests/fitfunctions/test_moyal.py +258 -0
  368. tests/fitfunctions/test_plots.py +258 -0
  369. tests/fitfunctions/test_power_laws.py +365 -0
  370. tests/fitfunctions/test_tex_info.py +183 -0
  371. tests/fitfunctions/test_trend_fit_properties.py +31 -0
  372. tests/fitfunctions/test_trend_fits.py +244 -0
  373. tests/plotting/__init__.py +1 -0
  374. tests/plotting/labels/__init__.py +1 -0
  375. tests/plotting/labels/test_chemistry.py +243 -0
  376. tests/plotting/labels/test_composition.py +345 -0
  377. tests/plotting/labels/test_datetime.py +445 -0
  378. tests/plotting/labels/test_elemental_abundance.py +366 -0
  379. tests/plotting/labels/test_init.py +66 -0
  380. tests/plotting/labels/test_labels_base.py +347 -0
  381. tests/plotting/labels/test_special.py +550 -0
  382. tests/plotting/test_agg_plot.py +602 -0
  383. tests/plotting/test_base.py +752 -0
  384. tests/plotting/test_fixtures_utilities.py +775 -0
  385. tests/plotting/test_histograms.py +546 -0
  386. tests/plotting/test_integration.py +675 -0
  387. tests/plotting/test_orbits.py +435 -0
  388. tests/plotting/test_performance.py +708 -0
  389. tests/plotting/test_scatter.py +752 -0
  390. tests/plotting/test_select_data_from_figure.py +1209 -0
  391. tests/plotting/test_spiral.py +573 -0
  392. tests/plotting/test_tools.py +607 -0
  393. tests/plotting/test_visual_validation.py +465 -0
  394. tests/solar_activity/__init__.py +1 -0
  395. tests/solar_activity/lisird/__init__.py +1 -0
  396. tests/solar_activity/lisird/test_extrema_calculator.py +593 -0
  397. tests/solar_activity/lisird/test_lisird_id.py +187 -0
  398. tests/solar_activity/sunspot_number/__init__.py +1 -0
  399. tests/solar_activity/sunspot_number/test_init.py +399 -0
  400. tests/solar_activity/sunspot_number/test_sidc.py +465 -0
  401. tests/solar_activity/sunspot_number/test_sidc_id.py +223 -0
  402. tests/solar_activity/sunspot_number/test_sidc_loader.py +275 -0
  403. tests/solar_activity/sunspot_number/test_ssn_extrema.py +406 -0
  404. tests/solar_activity/test_base.py +656 -0
  405. tests/solar_activity/test_init.py +396 -0
  406. tests/solar_activity/test_plots.py +371 -0
  407. tests/test_circular_imports.py +408 -0
  408. tests/test_issue_titles.py +25 -0
  409. tests/test_statusline.py +298 -0
  410. solarwindpy-0.0.1.dev0.dist-info/METADATA +0 -14
  411. solarwindpy-0.0.1.dev0.dist-info/RECORD +0 -4
  412. 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)