solarwindpy 0.1.2__py3-none-any.whl → 0.1.4__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.

@@ -240,9 +240,9 @@ class Hist2D(base.PlotWithZdata, base.CbarMaker, AggPlot):
240
240
  if axnorm is None:
241
241
  pass
242
242
  elif axnorm == "c":
243
- agg = agg.divide(agg.max(level="x"), level="x")
243
+ agg = agg.divide(agg.groupby(level="x").max(), level="x")
244
244
  elif axnorm == "r":
245
- agg = agg.divide(agg.max(level="y"), level="y")
245
+ agg = agg.divide(agg.groupby(level="y").max(), level="y")
246
246
  elif axnorm == "t":
247
247
  agg = agg.divide(agg.max())
248
248
  elif axnorm == "d":
@@ -265,7 +265,7 @@ class Hist2D(base.PlotWithZdata, base.CbarMaker, AggPlot):
265
265
 
266
266
  elif axnorm == "cd":
267
267
  # raise NotImplementedError("Need to verify data alignment, especially `dx` values and index")
268
- N = agg.sum(level="x")
268
+ N = agg.groupby(level="x").sum()
269
269
  dy = pd.IntervalIndex(
270
270
  agg.index.get_level_values("y").unique()
271
271
  ).sort_values()
@@ -275,7 +275,7 @@ class Hist2D(base.PlotWithZdata, base.CbarMaker, AggPlot):
275
275
 
276
276
  elif axnorm == "rd":
277
277
  # raise NotImplementedError("Need to verify data alignment, especially `dx` values and index")
278
- N = agg.sum(level="y")
278
+ N = agg.groupby(level="y").sum()
279
279
  dx = pd.IntervalIndex(
280
280
  agg.index.get_level_values("x").unique()
281
281
  ).sort_values()
@@ -286,9 +286,9 @@ class Hist2D(base.PlotWithZdata, base.CbarMaker, AggPlot):
286
286
  elif hasattr(axnorm, "__iter__"):
287
287
  kind, fcn = axnorm
288
288
  if kind == "c":
289
- agg = agg.divide(agg.agg(fcn, level="x"), level="x")
289
+ agg = agg.divide(agg.groupby(level="x").agg(fcn), level="x")
290
290
  elif kind == "r":
291
- agg = agg.divide(agg.agg(fcn, level="y"), level="y")
291
+ agg = agg.divide(agg.groupby(level="y").agg(fcn), level="y")
292
292
  else:
293
293
  raise ValueError(f"Unrecognized axnorm with function ({kind}, {fcn})")
294
294
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: solarwindpy
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Python package for solar wind data analysis.
5
5
  Author-email: "B. L. Alterman" <blaltermanphd@gmail.com>
6
6
  License: LICENSE
@@ -90,6 +90,23 @@ SolarWindPy
90
90
 
91
91
  Python data analysis tools for solar wind measurements.
92
92
 
93
+ Quick Start
94
+ -----------
95
+
96
+ After installation, import the package and create a plasma object:
97
+
98
+ .. code-block:: python
99
+
100
+ import solarwindpy as swp
101
+
102
+ # Load plasma data (example with sample data)
103
+ plasma = swp.Plasma()
104
+
105
+ # Access ion species and magnetic field data
106
+ print(plasma.data.columns) # View available measurements
107
+
108
+ See the documentation for detailed usage examples and API reference.
109
+
93
110
  Installation
94
111
  ============
95
112
 
@@ -116,17 +133,17 @@ Development
116
133
 
117
134
  .. code-block:: bash
118
135
 
119
- conda env create -f solarwindpy-20250403.yml # Python 3.10+
120
- conda activate solarwindpy-20250403
136
+ conda env create -f solarwindpy.yml # Python 3.10+
137
+ conda activate solarwindpy
121
138
  pip install -e .
122
139
 
123
140
  Alternatively generate the environment from ``requirements-dev.txt``:
124
141
 
125
142
  .. code-block:: bash
126
143
 
127
- python scripts/requirements_to_conda_env.py --name solarwindpy-dev
128
- conda env create -f solarwindpy-dev.yml
129
- conda activate solarwindpy-dev
144
+ python scripts/requirements_to_conda_env.py --name solarwindpy
145
+ conda env create -f solarwindpy.yml
146
+ conda activate solarwindpy
130
147
  pip install -e .
131
148
 
132
149
  3. Run the test suite with ``pytest``:
@@ -171,8 +188,8 @@ See `CITATION.rst`_ for instructions on citing SolarWindPy.
171
188
  .. _LICENSE.rst: ./LICENSE.rst
172
189
  .. _CITATION.rst: ./CITATION.rst
173
190
 
174
- .. |Build Status| image:: https://github.com/blalterman/SolarWindPy/actions/workflows/continuous-integration.yml/badge.svg?branch=master
175
- :target: https://github.com/blalterman/SolarWindPy/actions/workflows/continuous-integration.yml
191
+ .. |Build Status| image:: https://github.com/blalterman/SolarWindPy/actions/workflows/ci-master.yml/badge.svg?branch=master
192
+ :target: https://github.com/blalterman/SolarWindPy/actions/workflows/ci-master.yml
176
193
  .. |Docs Status| image:: https://readthedocs.org/projects/solarwindpy/badge/?version=latest
177
194
  :target: https://solarwindpy.readthedocs.io/en/latest/?badge=latest
178
195
  .. |License| image:: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
@@ -202,10 +202,13 @@ plans/documentation-workflow-fix/2-Configuration-Setup.md,sha256=alGWMMObb3aQp_k
202
202
  plans/documentation-workflow-fix/3-Pre-commit-Integration.md,sha256=7M-iyg1ifPikuWLYG8-M5EN9U6EnQektoD8J0tjTKfI,9066
203
203
  plans/documentation-workflow-fix/4-Workflow-Improvements.md,sha256=xs3dU5OCWksszqmXX44Y6qrLCNpoK_Q6b756Rsyqa1E,13130
204
204
  plans/documentation-workflow-fix/5-Documentation-and-Training.md,sha256=adnQVs7IhiAj6zNlzl_xjIfSVT35N95fDLlFEwlFdKQ,12725
205
- plans/github-issues-migration/0-Overview.md,sha256=KN7ig_ryX4zjc6xbJT8XuKXk9HJOdlGRvtVYU4xD4Fk,21939
206
- plans/github-issues-migration/1-Foundation-Label-System.md,sha256=fMrFDUGl82f2X7ceOYaTf5ajwrltoh6uU8FuNsn3_zY,8497
205
+ plans/github-issues-migration/0-Overview.md,sha256=u5-VQp8mE7zOOwixP6Z2ktXJC2HkaQ6BwKGQIGTmkvk,26114
206
+ plans/github-issues-migration/1-Foundation-Label-System.md,sha256=QVMyHVzj3xYQOmWBKdAVk-zYAOHJgG9-GYVUC3POJDE,11066
207
207
  plans/github-issues-migration/2-Migration-Tool-Rewrite.md,sha256=vXztSKQHKILm3IDgmvND1GgPqwfpXyGMA8lBfXiqY3E,11398
208
+ plans/github-issues-migration/2-Plan-Creation-System.md,sha256=uk8mLFSVCQO45AErRzwb_K0vJXf2yfX5lM1Tj4CFXnE,9612
208
209
  plans/github-issues-migration/3-CLI-Integration-Automation.md,sha256=PBvyP41Oa2lP7LBGMSkLXD0wfbZh1H1wyykRH-fZtCg,7504
210
+ plans/github-issues-migration/3-CLI-Integration-Documentation.md,sha256=0-YtPNYCEMUMtdoQob15-H7KTA3wOccTjE5rXgJDR2k,12117
211
+ plans/github-issues-migration/4-Plan-Closeout-Validation.md,sha256=SSLeNdAhgxYQAFzvt2PnQjrdkqO2q0cJem1JJa8flGI,10366
209
212
  plans/github-issues-migration/4-Validated-Migration.md,sha256=A0F6ybse7_OXUdJNAi49QDIkw8GoIBfFnhagyDZbI3Q,12036
210
213
  plans/github-issues-migration/5-Documentation-Training.md,sha256=1s6fSOZxe9SmAotH9xdZzUrd1iLTHm1LN92s46rIBqw,7967
211
214
  plans/github-issues-migration/6-Closeout.md,sha256=hrk93iv6AUPFULRx_YavqaJGQfn5vjhu-eAGBtpsYlo,7466
@@ -281,7 +284,6 @@ plans/tests-audit/artifacts/TEST_INVENTORY.md,sha256=fU4HaDHXsOPYMm41Jw4g9oG5OsT
281
284
  plans/tests-audit/artifacts/test_discovery_analysis.py,sha256=W57F6dZE8SVBmuoiQ7ccyNpoYE3OmgeOAZ-Z57gYpF0,9739
282
285
  plans/tests-audit/artifacts/test_parser.py,sha256=74PhaairFxZYgPFhPNe0Xgc_rj7mCOpNfYz4p9kKLiI,15910
283
286
  solarwindpy/README.md,sha256=54qoMZ3EDkwYIthS_fgolKE63_t5h6tl1fAu8W6sPWQ,63
284
- solarwindpy/Untitled.ipynb,sha256=25BEnYCUG4OiVgZjGRdJQBQRb7oUtl0R7mkZGiTkvfs,1589
285
287
  solarwindpy/__init__.py,sha256=41v4M-8UGLsWvG-y-QW1x6BBYYtHxi3WwhKyoW6eyuE,1490
286
288
  solarwindpy/core/__init__.py,sha256=gNQSJd3_NGaVg1cqgYEmP7a4AxcfJpEBB_9qEqcxwZM,489
287
289
  solarwindpy/core/alfvenic_turbulence.py,sha256=l2EChzYpAuuUFys1robjx9CEV9qY6eboIxIOTYXmosw,25730
@@ -298,7 +300,7 @@ solarwindpy/fitfunctions/exponentials.py,sha256=5NtpKMcns9BpD3jpLszw2Kn86Dt9Uhv4
298
300
  solarwindpy/fitfunctions/gaussians.py,sha256=db3yj89taBgHCjMF_GoxUlwJlJBjAfgH37rbCtZQZxc,8217
299
301
  solarwindpy/fitfunctions/lines.py,sha256=crs5CbdtMpz6liTeKcBW2_2o9dQClYPV5QXSrd5YXc8,2806
300
302
  solarwindpy/fitfunctions/moyal.py,sha256=cfoNGJ4-0tHjJUMKoVf7WXo4Zf_UwtYIa9b8WZd385k,2043
301
- solarwindpy/fitfunctions/plots.py,sha256=wMfQ_l9oBmIoEUFKtNe-7w8V-naz5cPVOTZXQ-rEE24,24487
303
+ solarwindpy/fitfunctions/plots.py,sha256=nnu1zLxkmHOe0HqDD_yZWIRhvZ6rvVG0hZakxsPvELk,24615
302
304
  solarwindpy/fitfunctions/power_laws.py,sha256=rEt-eO8k5HwFhqN1WKLF4fb-ADsQzFiFi6RfK-i-y90,6065
303
305
  solarwindpy/fitfunctions/tex_info.py,sha256=UXI-JFe6oIn4wNmknm6woyJzNVYolW4OiWHvaSCApHg,18919
304
306
  solarwindpy/fitfunctions/trend_fits.py,sha256=ZAsHGpMf5ATcCTHkaLglsTqDsTA6wcY2UoWlN3YKv3U,15353
@@ -309,7 +311,7 @@ solarwindpy/plotting/__init__.py,sha256=xcyOoL2mt9uO0HBgtyXbZMF2pWP1OvL9EbG8fl5P
309
311
  solarwindpy/plotting/agg_plot.py,sha256=NhEx2Kh4dDGxBdMjJVsCuMwRyIvtWS5lNi-TlbbFrnM,16381
310
312
  solarwindpy/plotting/base.py,sha256=FhyLeQnAgLOyjdJAWLIswusCLvFlyQQxTSufnMEw_cQ,14345
311
313
  solarwindpy/plotting/hist1d.py,sha256=tynZSnFfjw53pVFki6EXbti3NP5q62zp8xQD6NFhDrE,12721
312
- solarwindpy/plotting/hist2d.py,sha256=Z8kr4RPqAZ1gA9G9reJd1zzRs6fHecY_z1oKMOHTkDA,33901
314
+ solarwindpy/plotting/hist2d.py,sha256=AEDcotXoWsGrojr6GsvGCRMClZzXGu7j79eXqkJ0hqs,33957
313
315
  solarwindpy/plotting/histograms.py,sha256=vlViwlEohbWmnyXyEkaS5SW33NhCwz_WGsg5p4UgMsE,60538
314
316
  solarwindpy/plotting/orbits.py,sha256=s7kTtaWSD6fDKjtLnfIgAHOoNtAQc_1NR8a-Rj9d7-w,17011
315
317
  solarwindpy/plotting/scatter.py,sha256=m8tCkOwapRppOREfR8Gvw6UZxRxmWgclkjaxbKPIYUI,2746
@@ -337,7 +339,7 @@ solarwindpy/solar_activity/sunspot_number/sidc.py,sha256=zTktSZxPSNtdQ1jrRl7V76k
337
339
  solarwindpy/solar_activity/sunspot_number/ssn_extrema.csv,sha256=5OJ5iEMLWIYYnBqihYpDBAOV9oiwE3hLmVzEsUUlfr4,2101
338
340
  solarwindpy/solar_activity/sunspot_number/ssn_extrema.csv.silso,sha256=iQq8Ea0EMU1LZj2wtJQeRd2Sw-PW58wGSJtVpR8hgmE,2102
339
341
  solarwindpy/tools/__init__.py,sha256=hoLkvWMHNcDFlxdjKF8wov4MNzHVCayheiRcrYCZ7RM,4602
340
- solarwindpy-0.1.2.dist-info/licenses/LICENSE.rst,sha256=LnFDisrg2iF2g1WG7hg3UyezM7BHvixBj5u-KJy7HqI,1531
342
+ solarwindpy-0.1.4.dist-info/licenses/LICENSE.rst,sha256=LnFDisrg2iF2g1WG7hg3UyezM7BHvixBj5u-KJy7HqI,1531
341
343
  tests/__init__.py,sha256=QMXatjFn0cwLVfV0syJuu_di6qEr8qYe-WwhwUBssLg,32
342
344
  tests/conftest.py,sha256=sPZrGyQIdS6-i9LGbXOors23BId5p7TCoyJaF8whxOw,441
343
345
  tests/test_circular_imports.py,sha256=S94K4RRKggn_9rJutfyTXBTMtPxbG3DQNcR6HMHOHdU,14664
@@ -353,7 +355,7 @@ tests/core/test_ions.py,sha256=AXBhG5ANI2cKr1rfzbbFwKFGBghznAXdi3jfOsIZq4M,10434
353
355
  tests/core/test_plasma.py,sha256=bI_rDyWpnM2G78M4KRqEfwi82i_xRuqPx4WaNSKTGHA,103241
354
356
  tests/core/test_plasma_io.py,sha256=j0TrV5T2rPMcKX7tbIzWO_DWTwmJ96hnwrmazFOGL4k,384
355
357
  tests/core/test_quantities.py,sha256=_9cP3HdVr0JMUDABZiwTSnBokSpZbWagqFQ-OSrL2F8,16955
356
- tests/core/test_spacecraft.py,sha256=jYh1VivdkL4ZnC72WlVamHJr4D_8t8KiY4lrpWqGejE,6815
358
+ tests/core/test_spacecraft.py,sha256=sYXtPVgGaGZoRHP6rgoE8jv-ZdBfbPtYH0Ft0WgNkWM,6855
357
359
  tests/core/test_units_constants.py,sha256=Yelx_3F6Q1IH-yOLJDgcsF4khYu6nZhVY7svm3xnHAk,511
358
360
  tests/data/epoch.csv,sha256=ILnMSC8BUwtYzOedeldDFOtB4MYEvZgumCT8LZLoa90,72
359
361
  tests/data/plasma.csv,sha256=jtT4msNHX_muY7YWRQkWIrqU140nmnLLLstHSoAhs_w,652
@@ -373,6 +375,7 @@ tests/plotting/__init__.py,sha256=5R4PdTYEQvYbU0yuz2a1EcFlzic8QUHnl_ojA9tmFzY,24
373
375
  tests/plotting/test_agg_plot.py,sha256=lBrNSgwEHcDtCD6YM8_yV_hFdxlOCCWaxIV_dnF-ZJI,21156
374
376
  tests/plotting/test_base.py,sha256=vyHl4vSn71B7BvcFcLNCjc1FCCvELFKcBkbt1PhXSu4,24618
375
377
  tests/plotting/test_fixtures_utilities.py,sha256=SvjOixxm-eXvSkEtjOAMVn_UmHKkEB2Kc9W0fI7ZzlU,25483
378
+ tests/plotting/test_hist2d_pandas_compat.py,sha256=RYm9xCc7XyHv8AJffcfH3Kqrr9Y3bg2dEMYD5rIqsO0,15205
376
379
  tests/plotting/test_histograms.py,sha256=QSyA0lgc8x8t5Niwtuqveco8DV8kGHfEoV5ZhAjNfRk,20007
377
380
  tests/plotting/test_integration.py,sha256=_YNuQ2x_7gx_m2-8JWOmMBrJimalFll53-phEtb1yjU,22964
378
381
  tests/plotting/test_orbits.py,sha256=AjS5mk-69LjmJvnrIcX3NcQW6dyVGEy0ZmeoYhQ79Ws,16381
@@ -381,7 +384,7 @@ tests/plotting/test_scatter.py,sha256=Neco0LuCsRfDB7nwyMSQsnqxlvpUi2lCRZlkXGQZ2u
381
384
  tests/plotting/test_select_data_from_figure.py,sha256=Ndz-mtsB8FZXrzOb8IWhMynWhpt8CqaQSOKVpugK_co,42427
382
385
  tests/plotting/test_spiral.py,sha256=GrKRxjdIa5f9wnS0EZtkjS13h5OW8mvBIlgZY7UtSfY,20494
383
386
  tests/plotting/test_tools.py,sha256=fptKZFwCMq5vOIeoZikTqoXI87dGr_kiOwAEXTVX_vE,20383
384
- tests/plotting/test_visual_validation.py,sha256=Sp7IMBaGKQM6ja4cIuGDa9MPhmwlmRJfPEOPIOjTYkQ,14692
387
+ tests/plotting/test_visual_validation.py,sha256=rRu_Fj1d22GIG6k1ByC2-nCKgO5ONuETsaLcSaSDEE8,14700
385
388
  tests/plotting/labels/__init__.py,sha256=p1Ttswdbhq9cxnuaQlQ1LSPbb-92OBwzLlWcCz5ACFg,24
386
389
  tests/plotting/labels/test_chemistry.py,sha256=AawZXmN3f72NKrkgybDm342NvepKLS55-jaNDRsjjmE,7950
387
390
  tests/plotting/labels/test_composition.py,sha256=ps1Mnmey8UfxvCs-mNR1P-1j9kgf2ImLO7gX3PmuEw0,11939
@@ -403,7 +406,7 @@ tests/solar_activity/sunspot_number/test_sidc.py,sha256=VDTC3KIZNrs8HqSp7qNnz1K4
403
406
  tests/solar_activity/sunspot_number/test_sidc_id.py,sha256=xP_dLw99dPxAKhW-YqcextUAvNNLrRC9VgWVm0JxBPo,8153
404
407
  tests/solar_activity/sunspot_number/test_sidc_loader.py,sha256=4LmOmyt9OLas2oUT9iOa7TQ8bn5iyB9a_HqupIH05OQ,10271
405
408
  tests/solar_activity/sunspot_number/test_ssn_extrema.py,sha256=KOliGq5ErhKYWp_qbXmBqZU4EmHS-bOgmBcAngIkQfQ,14747
406
- solarwindpy-0.1.2.dist-info/METADATA,sha256=d1glzMiPkgHzEoWLcDCZOkn4V5eRRKSyYhb5Co2Mkj8,6336
407
- solarwindpy-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
408
- solarwindpy-0.1.2.dist-info/top_level.txt,sha256=kKjGFlQvA-UE4SpU-m7df_Y1_eGhtRs_eqy6Gq96v8c,24
409
- solarwindpy-0.1.2.dist-info/RECORD,,
409
+ solarwindpy-0.1.4.dist-info/METADATA,sha256=8QgE83e8dUPDpgXWvvit0dxiwmc-Gmx7BoEorRfCjXA,6688
410
+ solarwindpy-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
411
+ solarwindpy-0.1.4.dist-info/top_level.txt,sha256=kKjGFlQvA-UE4SpU-m7df_Y1_eGhtRs_eqy6Gq96v8c,24
412
+ solarwindpy-0.1.4.dist-info/RECORD,,
@@ -16,7 +16,7 @@ from solarwindpy import spacecraft
16
16
  pd.set_option("mode.chained_assignment", "raise")
17
17
 
18
18
 
19
- class TestBase(ABC):
19
+ class SpacecraftTestBase(ABC):
20
20
  @classmethod
21
21
  def setUpClass(cls):
22
22
  data = base.TestData()
@@ -47,7 +47,7 @@ class TestBase(ABC):
47
47
  # cls.set_object_testing()
48
48
  # print("Done with TestBase", flush=True)
49
49
 
50
- # super(TestBase, cls).setUpClass()
50
+ # super(SpacecraftTestBase, cls).setUpClass()
51
51
  # # print(cls.data.iloc[:, :7])
52
52
  # # print(cls.data.columns.values)
53
53
  # cls.data = cls.spacecraft_data
@@ -127,7 +127,7 @@ class TestBase(ABC):
127
127
  pdt.assert_series_equal(dist, ot.distance2sun)
128
128
 
129
129
 
130
- class TestWind(TestBase, TestCase):
130
+ class TestWind(SpacecraftTestBase, TestCase):
131
131
  @classmethod
132
132
  def set_object_testing(cls):
133
133
  data = cls.data.xs("gse", axis=1, level="M")
@@ -164,7 +164,7 @@ class TestWind(TestBase, TestCase):
164
164
  self.object_testing.carrington
165
165
 
166
166
 
167
- class TestPSP(TestBase, TestCase):
167
+ class TestPSP(SpacecraftTestBase, TestCase):
168
168
  @classmethod
169
169
  def set_object_testing(cls):
170
170
  p = cls.data.xs("pos_HCI", axis=1, level="M")
@@ -0,0 +1,409 @@
1
+ #!/usr/bin/env python
2
+ """Regression tests for pandas 2.3.1+ compatibility in hist2d.py.
3
+
4
+ These tests verify that the axis normalization methods work correctly
5
+ after replacing deprecated .max(level=...) syntax with .groupby(level=...).max().
6
+ """
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ import pytest
11
+
12
+ from solarwindpy.plotting.hist2d import Hist2D
13
+
14
+
15
+ class TestHist2DPandasCompatibility:
16
+ """Test pandas 2.3.1+ compatibility for Hist2D axis normalization."""
17
+
18
+ def setup_method(self):
19
+ """Set up test data for each test."""
20
+ np.random.seed(42)
21
+ n = 1000
22
+
23
+ # Create test data with known distributions
24
+ self.x_data = pd.Series(np.random.normal(0, 1, n), name="x")
25
+ self.y_data = pd.Series(np.random.normal(0, 1, n), name="y")
26
+ self.z_data = pd.Series(np.random.uniform(0, 10, n), name="z")
27
+
28
+ def test_column_normalize(self):
29
+ """Test column normalize (axnorm='c')."""
30
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
31
+ hist.set_axnorm("c")
32
+
33
+ # Get normalized aggregation
34
+ agg = hist.agg()
35
+ agg_unstacked = agg.unstack("x")
36
+
37
+ # Check that max value in each column is 1.0 (or NaN)
38
+ for col in agg_unstacked.columns:
39
+ col_max = agg_unstacked[col].max()
40
+ if not pd.isna(col_max):
41
+ assert np.isclose(
42
+ col_max, 1.0, atol=1e-10
43
+ ), f"Column {col} max is {col_max}, expected 1.0"
44
+
45
+ # Check that all non-NaN values are between 0 and 1
46
+ non_nan_values = agg.dropna()
47
+ assert (
48
+ non_nan_values >= 0
49
+ ).all(), "Found negative values after column normalization"
50
+ assert (
51
+ non_nan_values <= 1.0001
52
+ ).all(), "Found values > 1 after column normalization"
53
+
54
+ def test_row_normalize(self):
55
+ """Test row normalize (axnorm='r')."""
56
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
57
+ hist.set_axnorm("r")
58
+
59
+ # Get normalized aggregation
60
+ agg = hist.agg()
61
+ agg_unstacked = agg.unstack("x")
62
+
63
+ # Check that max value in each row is 1.0 (or NaN)
64
+ for row_idx in agg_unstacked.index:
65
+ row_max = agg_unstacked.loc[row_idx].max()
66
+ if not pd.isna(row_max):
67
+ assert np.isclose(
68
+ row_max, 1.0, atol=1e-10
69
+ ), f"Row {row_idx} max is {row_max}, expected 1.0"
70
+
71
+ # Check that all non-NaN values are between 0 and 1
72
+ non_nan_values = agg.dropna()
73
+ assert (
74
+ non_nan_values >= 0
75
+ ).all(), "Found negative values after row normalization"
76
+ assert (
77
+ non_nan_values <= 1.0001
78
+ ).all(), "Found values > 1 after row normalization"
79
+
80
+ def test_total_normalize(self):
81
+ """Test total normalize (axnorm='t')."""
82
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
83
+ hist.set_axnorm("t")
84
+
85
+ # Get normalized aggregation
86
+ agg = hist.agg()
87
+
88
+ # Check that max value overall is 1.0
89
+ max_val = agg.max()
90
+ assert np.isclose(
91
+ max_val, 1.0, atol=1e-10
92
+ ), f"Total max is {max_val}, expected 1.0"
93
+
94
+ # Check that all non-NaN values are between 0 and 1
95
+ non_nan_values = agg.dropna()
96
+ assert (
97
+ non_nan_values >= 0
98
+ ).all(), "Found negative values after total normalization"
99
+ assert (
100
+ non_nan_values <= 1.0001
101
+ ).all(), "Found values > 1 after total normalization"
102
+
103
+ def test_density_normalize(self):
104
+ """Test density normalize (axnorm='d').
105
+
106
+ This should create a true 2D probability density function where
107
+ the integral over the entire domain equals 1.
108
+ """
109
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
110
+ hist.set_axnorm("d")
111
+
112
+ # Get normalized aggregation
113
+ agg = hist.agg()
114
+
115
+ # Get bin widths for integration
116
+ x_intervals = hist.intervals["x"]
117
+ y_intervals = hist.intervals["y"]
118
+
119
+ # Calculate dx and dy for each bin
120
+ dx_values = pd.Series(
121
+ [interval.length for interval in x_intervals], index=x_intervals
122
+ )
123
+ dy_values = pd.Series(
124
+ [interval.length for interval in y_intervals], index=y_intervals
125
+ )
126
+
127
+ # Compute the integral: sum(agg * dx * dy)
128
+ agg_unstacked = agg.unstack("x")
129
+ total_integral = 0
130
+ for y_idx, y_interval in enumerate(agg_unstacked.index):
131
+ for x_idx, x_interval in enumerate(agg_unstacked.columns):
132
+ value = agg_unstacked.iloc[y_idx, x_idx]
133
+ if not pd.isna(value):
134
+ dx = dx_values[x_interval]
135
+ dy = dy_values[y_interval]
136
+ total_integral += value * dx * dy
137
+
138
+ # The integral should be close to 1
139
+ assert np.isclose(
140
+ total_integral, 1.0, atol=0.01
141
+ ), f"Density integral is {total_integral}, expected 1.0"
142
+
143
+ # Values should be non-negative
144
+ non_nan_values = agg.dropna()
145
+ assert (
146
+ non_nan_values >= 0
147
+ ).all(), "Found negative values after density normalization"
148
+
149
+ def test_pdfs_in_each_column(self):
150
+ """Test PDFs in each column (axnorm='cd').
151
+
152
+ This creates PDFs in each column, so integrating over y for each x should give 1.
153
+ """
154
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
155
+ hist.set_axnorm("cd")
156
+
157
+ # Get normalized aggregation
158
+ agg = hist.agg()
159
+ agg_unstacked = agg.unstack("x")
160
+
161
+ # Get y bin widths for integration
162
+ y_intervals = hist.intervals["y"]
163
+ dy_values = pd.Series(
164
+ [interval.length for interval in y_intervals], index=y_intervals
165
+ )
166
+
167
+ # For each column, integrate over y
168
+ for col in agg_unstacked.columns:
169
+ col_data = agg_unstacked[col]
170
+ integral = 0
171
+ for y_idx, y_interval in enumerate(col_data.index):
172
+ value = col_data.iloc[y_idx]
173
+ if not pd.isna(value):
174
+ dy = dy_values[y_interval]
175
+ integral += value * dy
176
+
177
+ # Each column should integrate to 1 (if it has data)
178
+ if integral > 0: # Only check columns with data
179
+ assert np.isclose(
180
+ integral, 1.0, atol=0.01
181
+ ), f"Column {col} PDF integral is {integral}, expected 1.0"
182
+
183
+ # Values should be non-negative
184
+ non_nan_values = agg.dropna()
185
+ assert (
186
+ non_nan_values >= 0
187
+ ).all(), "Found negative values after PDFs in each column"
188
+
189
+ def test_pdfs_in_each_row(self):
190
+ """Test PDFs in each row (axnorm='rd').
191
+
192
+ This creates PDFs in each row, so integrating over x for each y should give 1.
193
+ """
194
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
195
+ hist.set_axnorm("rd")
196
+
197
+ # Get normalized aggregation
198
+ agg = hist.agg()
199
+ agg_unstacked = agg.unstack("x")
200
+
201
+ # Get x bin widths for integration
202
+ x_intervals = hist.intervals["x"]
203
+ dx_values = pd.Series(
204
+ [interval.length for interval in x_intervals], index=x_intervals
205
+ )
206
+
207
+ # For each row, integrate over x
208
+ for row in agg_unstacked.index:
209
+ row_data = agg_unstacked.loc[row]
210
+ integral = 0
211
+ for x_idx, x_interval in enumerate(row_data.index):
212
+ value = row_data.iloc[x_idx]
213
+ if not pd.isna(value):
214
+ dx = dx_values[x_interval]
215
+ integral += value * dx
216
+
217
+ # Each row should integrate to 1 (if it has data)
218
+ if integral > 0: # Only check rows with data
219
+ assert np.isclose(
220
+ integral, 1.0, atol=0.01
221
+ ), f"Row {row} PDF integral is {integral}, expected 1.0"
222
+
223
+ # Values should be non-negative
224
+ non_nan_values = agg.dropna()
225
+ assert (
226
+ non_nan_values >= 0
227
+ ).all(), "Found negative values after PDFs in each row"
228
+
229
+ def test_no_normalization(self):
230
+ """Test that no normalization (axnorm=None) works correctly."""
231
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
232
+ hist.set_axnorm(None)
233
+
234
+ # Get aggregation without normalization
235
+ agg = hist.agg()
236
+
237
+ # Values should be the raw aggregated z values
238
+ assert agg is not None
239
+ assert not agg.isna().all(), "All values are NaN without normalization"
240
+
241
+ # Check that values are in reasonable range for raw z data (0-10)
242
+ non_nan_values = agg.dropna()
243
+ assert non_nan_values.min() >= 0, "Found negative values in raw aggregation"
244
+ assert (
245
+ non_nan_values.max() <= 10.1
246
+ ), "Found unexpectedly large values in raw aggregation"
247
+
248
+ def test_edge_case_single_bin(self):
249
+ """Test normalization with data that falls into a single bin."""
250
+ # Create data that falls into one bin
251
+ x_single = pd.Series([0.5] * 100, name="x")
252
+ y_single = pd.Series([0.5] * 100, name="y")
253
+ z_single = pd.Series(np.random.uniform(1, 2, 100), name="z")
254
+
255
+ hist = Hist2D(x_single, y_single, z_single, nbins=10)
256
+ hist.set_axnorm("c")
257
+
258
+ # Get normalized aggregation
259
+ agg = hist.agg()
260
+
261
+ # Should have mostly NaN except for the single bin
262
+ non_nan_count = agg.notna().sum()
263
+ assert non_nan_count == 1, f"Expected 1 non-NaN value, got {non_nan_count}"
264
+
265
+ # The single value should be 1.0 after normalization
266
+ non_nan_value = agg.dropna().iloc[0]
267
+ assert np.isclose(
268
+ non_nan_value, 1.0, atol=1e-10
269
+ ), f"Single bin value is {non_nan_value}, expected 1.0"
270
+
271
+ def test_edge_case_with_nans(self):
272
+ """Test normalization with NaN values in input data."""
273
+ # Add some NaN values to the data
274
+ x_with_nan = self.x_data.copy()
275
+ y_with_nan = self.y_data.copy()
276
+ z_with_nan = self.z_data.copy()
277
+
278
+ # Insert NaNs at random positions
279
+ nan_indices = np.random.choice(len(x_with_nan), 50, replace=False)
280
+ x_with_nan.iloc[nan_indices] = np.nan
281
+ y_with_nan.iloc[nan_indices[:25]] = np.nan
282
+ z_with_nan.iloc[nan_indices[25:]] = np.nan
283
+
284
+ hist = Hist2D(x_with_nan, y_with_nan, z_with_nan, nbins=10)
285
+ hist.set_axnorm("c")
286
+
287
+ # Should handle NaNs gracefully
288
+ agg = hist.agg()
289
+ assert agg is not None
290
+
291
+ # Check that non-NaN values are properly normalized
292
+ non_nan_values = agg.dropna()
293
+ if len(non_nan_values) > 0:
294
+ assert (non_nan_values >= 0).all(), "Found negative values with NaN input"
295
+ assert (non_nan_values <= 1.0001).all(), "Found values > 1 with NaN input"
296
+
297
+ def test_count_aggregation_with_column_normalize(self):
298
+ """Test column normalize with count aggregation (no z values)."""
299
+ # Create hist2d without z values (count aggregation)
300
+ hist = Hist2D(self.x_data, self.y_data, nbins=10)
301
+ hist.set_axnorm("c")
302
+
303
+ # Get normalized aggregation
304
+ agg = hist.agg()
305
+ agg_unstacked = agg.unstack("x")
306
+
307
+ # Check that max value in each column is 1.0 (or NaN)
308
+ for col in agg_unstacked.columns:
309
+ col_max = agg_unstacked[col].max()
310
+ if not pd.isna(col_max):
311
+ assert np.isclose(
312
+ col_max, 1.0, atol=1e-10
313
+ ), f"Column {col} max is {col_max}, expected 1.0"
314
+
315
+ def test_log_scale_with_normalization(self):
316
+ """Test normalization with log-scaled data."""
317
+ # Use positive data for log scale
318
+ x_positive = pd.Series(np.random.uniform(1, 100, 1000), name="x")
319
+ y_positive = pd.Series(np.random.uniform(1, 100, 1000), name="y")
320
+ z_positive = pd.Series(np.random.uniform(1, 10, 1000), name="z")
321
+
322
+ hist = Hist2D(
323
+ x_positive, y_positive, z_positive, nbins=10, logx=True, logy=True
324
+ )
325
+ hist.set_axnorm("c")
326
+
327
+ # Get normalized aggregation
328
+ agg = hist.agg()
329
+
330
+ # Check normalization works with log scale
331
+ assert agg is not None
332
+ assert not agg.isna().all(), "All values are NaN with log scale"
333
+
334
+ # Check that values are properly normalized
335
+ non_nan_values = agg.dropna()
336
+ assert (non_nan_values >= 0).all(), "Found negative values with log scale"
337
+ assert (non_nan_values <= 1.0001).all(), "Found values > 1 with log scale"
338
+
339
+ def test_all_documented_normalizations(self):
340
+ """Test that all documented normalization options work without error."""
341
+ documented_options = [
342
+ (None, "no normalization"),
343
+ ("c", "column normalize"),
344
+ ("r", "row normalize"),
345
+ ("t", "total normalize"),
346
+ ("d", "density normalize"),
347
+ ("cd", "PDFs in each column"),
348
+ ("rd", "PDFs in each row"),
349
+ ]
350
+
351
+ for option, description in documented_options:
352
+ hist = Hist2D(self.x_data, self.y_data, self.z_data, nbins=10)
353
+ hist.set_axnorm(option)
354
+
355
+ # Should be able to get aggregation without error
356
+ agg = hist.agg()
357
+ assert (
358
+ agg is not None
359
+ ), f"Failed to get aggregation with axnorm={option} ({description})"
360
+
361
+ # Should have at least some non-NaN values
362
+ if option is not None: # None means no normalization
363
+ non_nan_count = agg.notna().sum()
364
+ assert (
365
+ non_nan_count > 0
366
+ ), f"No non-NaN values with axnorm={option} ({description})"
367
+
368
+ def test_density_normalize_with_counts(self):
369
+ """Test density normalize with count data (no z values).
370
+
371
+ This should create a proper 2D probability density where integral = 1.
372
+ """
373
+ hist = Hist2D(self.x_data, self.y_data, nbins=10)
374
+ hist.set_axnorm("d")
375
+
376
+ # Get normalized aggregation
377
+ agg = hist.agg()
378
+
379
+ # Get bin widths for integration
380
+ x_intervals = hist.intervals["x"]
381
+ y_intervals = hist.intervals["y"]
382
+
383
+ # Calculate dx and dy for each bin
384
+ dx_values = pd.Series(
385
+ [interval.length for interval in x_intervals], index=x_intervals
386
+ )
387
+ dy_values = pd.Series(
388
+ [interval.length for interval in y_intervals], index=y_intervals
389
+ )
390
+
391
+ # Compute the integral
392
+ agg_unstacked = agg.unstack("x")
393
+ total_integral = 0
394
+ for y_idx, y_interval in enumerate(agg_unstacked.index):
395
+ for x_idx, x_interval in enumerate(agg_unstacked.columns):
396
+ value = agg_unstacked.iloc[y_idx, x_idx]
397
+ if not pd.isna(value):
398
+ dx = dx_values[x_interval]
399
+ dy = dy_values[y_interval]
400
+ total_integral += value * dx * dy
401
+
402
+ # The integral should be close to 1
403
+ assert np.isclose(
404
+ total_integral, 1.0, atol=0.01
405
+ ), f"Count density integral is {total_integral}, expected 1.0"
406
+
407
+
408
+ if __name__ == "__main__":
409
+ pytest.main([__file__, "-v"])
@@ -22,7 +22,7 @@ class TestVisualValidationFramework:
22
22
  def test_matplotlib_backend(self):
23
23
  """Test matplotlib backend is properly configured."""
24
24
  backend = matplotlib.get_backend()
25
- assert backend == "Agg", f"Expected Agg backend, got {backend}"
25
+ assert backend.lower() == "agg", f"Expected Agg backend, got {backend}"
26
26
 
27
27
  def test_matplotlib_interactive_mode(self):
28
28
  """Test matplotlib interactive mode is disabled."""