nullcal 0.2.0__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.
Files changed (42) hide show
  1. nullcal/__init__.py +16 -0
  2. nullcal/calibration.py +189 -0
  3. nullcal/clustering/__init__.py +0 -0
  4. nullcal/clustering/base.py +39 -0
  5. nullcal/clustering/injection.py +63 -0
  6. nullcal/clustering/precompute.py +30 -0
  7. nullcal/clustering/single.py +208 -0
  8. nullcal/clustering/time_frequency_map.py +42 -0
  9. nullcal/data.py +93 -0
  10. nullcal/likelihood/__init__.py +9 -0
  11. nullcal/likelihood/recalibration_likelihood.py +227 -0
  12. nullcal/metadata/__init__.py +0 -0
  13. nullcal/metadata/yaml.py +30 -0
  14. nullcal/null_stream/__init__.py +0 -0
  15. nullcal/null_stream/calibration.py +40 -0
  16. nullcal/null_stream/null_stream.py +252 -0
  17. nullcal/null_stream/projector.py +48 -0
  18. nullcal/null_stream/whiten.py +106 -0
  19. nullcal/result/__init__.py +5 -0
  20. nullcal/result/result.py +50 -0
  21. nullcal/result/utils.py +13 -0
  22. nullcal/sampler.py +113 -0
  23. nullcal/studies/__init__.py +1 -0
  24. nullcal/studies/lwa_leakage.py +513 -0
  25. nullcal/studies/spline_resolution.py +148 -0
  26. nullcal/time_frequency_transform/README.md +7 -0
  27. nullcal/time_frequency_transform/__init__.py +23 -0
  28. nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.py +42 -0
  29. nullcal/time_frequency_transform/inverse_wavelet_time_funcs.py +49 -0
  30. nullcal/time_frequency_transform/stft.py +45 -0
  31. nullcal/time_frequency_transform/transform_freq_funcs.py +180 -0
  32. nullcal/time_frequency_transform/transform_time_funcs.py +60 -0
  33. nullcal/time_frequency_transform/utils.py +21 -0
  34. nullcal/time_frequency_transform/wavelet_transforms.py +249 -0
  35. nullcal/utils/__init__.py +0 -0
  36. nullcal/utils/log.py +72 -0
  37. nullcal/utils/snr.py +29 -0
  38. nullcal/version.py +9 -0
  39. nullcal-0.2.0.dist-info/METADATA +222 -0
  40. nullcal-0.2.0.dist-info/RECORD +42 -0
  41. nullcal-0.2.0.dist-info/WHEEL +4 -0
  42. nullcal-0.2.0.dist-info/licenses/LICENSE +21 -0
nullcal/utils/log.py ADDED
@@ -0,0 +1,72 @@
1
+ """
2
+ Functions for logging.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from pathlib import Path
9
+
10
+ from ..version import __version__
11
+
12
+
13
+ def get_version_information() -> str:
14
+ """Get the version information.
15
+
16
+ Returns:
17
+ str: Version information.
18
+ """
19
+
20
+ return __version__
21
+
22
+
23
+ def setup_logger(
24
+ outdir: str = ".", label: str | None = None, log_level: str | int = "INFO", print_version: bool = False
25
+ ):
26
+ """Setup logging output: call at the start of the script to use
27
+
28
+ Args:
29
+ outdir (str, optional): If supplied, write the logging output to outdir/label.log. Defaults to '.'.
30
+ label (str, optional): If supplied, write the logging output to outdir/label.log. Defaults to None.
31
+ log_level (str, optional): ['debug', 'info', 'warning']
32
+ Either a string from the list above, or an integer as specified
33
+ in https://docs.python.org/2/library/logging.html#logging-levels
34
+ Defaults to 'INFO'.
35
+ print_version (bool): If true, print version information. Defaults to False.
36
+ """
37
+
38
+ if isinstance(log_level, str):
39
+ try:
40
+ level = getattr(logging, log_level.upper())
41
+ except AttributeError as err:
42
+ raise ValueError(f"log_level {log_level} not understood") from err
43
+ else:
44
+ level = int(log_level)
45
+
46
+ logger = logging.getLogger("nullcal")
47
+ logger.propagate = False
48
+ logger.setLevel(level)
49
+
50
+ if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
51
+ stream_handler = logging.StreamHandler()
52
+ stream_handler.setFormatter(
53
+ logging.Formatter("%(asctime)s %(name)s %(levelname)-8s: %(message)s", datefmt="%H:%M")
54
+ )
55
+ stream_handler.setLevel(level)
56
+ logger.addHandler(stream_handler)
57
+
58
+ if not any(isinstance(h, logging.FileHandler) for h in logger.handlers) and label:
59
+ Path(outdir).mkdir(parents=True, exist_ok=True)
60
+ log_file = f"{outdir}/{label}.log"
61
+ file_handler = logging.FileHandler(log_file)
62
+ file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)-8s: %(message)s", datefmt="%H:%M"))
63
+
64
+ file_handler.setLevel(level)
65
+ logger.addHandler(file_handler)
66
+
67
+ for handler in logger.handlers:
68
+ handler.setLevel(level)
69
+
70
+ if print_version:
71
+ version = get_version_information()
72
+ logger.info("Running nullcal version: %s", version)
nullcal/utils/snr.py ADDED
@@ -0,0 +1,29 @@
1
+ """
2
+ Helper functions to compute signal-to-noise ratio.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import numpy as np
8
+
9
+
10
+ def optimal_uncorrelated_null_stream_snr_squared(signals, power_spectral_densities, duration):
11
+ """Compute the optimal uncorrelated null stream
12
+ signal-to-noise ratio. The interferometers are
13
+ assumed to be uncorrelated to each other.
14
+
15
+ Args:
16
+ signals (array-like): Frequency domain signals in the interferometers (detector, frequency).
17
+ power_spectral_densities (array-like): Noise power spectral densities of the interferometers
18
+ (detector, frequency).
19
+ duration (float): Duration in second.
20
+
21
+ Returns:
22
+ float: Optimal signal-to-noise ratio squared of uncorrelated null stream.
23
+ """
24
+ # Direct sum of the signals
25
+ null_stream = np.sum(signals, axis=0) / np.sqrt(len(signals))
26
+
27
+ # Power spectral density
28
+ power_spectral_density = np.sum(power_spectral_densities, axis=0) / len(power_spectral_densities)
29
+ return 4.0 / duration * np.sum(np.conj(null_stream) * null_stream / power_spectral_density)
nullcal/version.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ Version of the package.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from importlib.metadata import version
8
+
9
+ __version__ = version("nullcal")
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.5
2
+ Name: nullcal
3
+ Version: 0.2.0
4
+ Summary: A package to constrain calibration errors of a closed-geometry network of gravitational-wave detectors.
5
+ Project-URL: Source, https://github.com/Leuven-Gravity-Institute/nullcal
6
+ Project-URL: Tracker, https://github.com/Leuven-Gravity-Institute/nullcal/issues
7
+ Author-email: "Isaac C. F. Wong" <chunfung.wong@kuleuven.be>, Milan Wils <milan.wils@kuleuven.be>, Francesco Cireddu <francesco.cireddu@kuleuven.be>
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 Chun-Fung Wong
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+ License-File: LICENSE
30
+ Classifier: Development Status :: 3 - Alpha
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3 :: Only
34
+ Classifier: Programming Language :: Python :: 3.12
35
+ Classifier: Programming Language :: Python :: 3.13
36
+ Classifier: Programming Language :: Python :: 3.14
37
+ Requires-Python: >=3.12
38
+ Requires-Dist: blackjax>=1.6.2
39
+ Requires-Dist: jax>=0.11.1
40
+ Requires-Dist: numpy>=1.26.4
41
+ Requires-Dist: pandas>=2.3.3
42
+ Requires-Dist: pyyaml>=6.0.3
43
+ Requires-Dist: scipy>=1.16.3
44
+ Provides-Extra: cuda
45
+ Requires-Dist: jax[cuda13]>=0.11.1; (sys_platform == 'linux') and extra == 'cuda'
46
+ Provides-Extra: jax
47
+ Requires-Dist: jax>=0.11.1; extra == 'jax'
48
+ Description-Content-Type: text/markdown
49
+
50
+ # nullcal
51
+
52
+ [![Python CI](https://github.com/Leuven-Gravity-Institute/nullcal/actions/workflows/ci.yml/badge.svg)](https://github.com/Leuven-Gravity-Institute/nullcal/actions/workflows/ci.yml)
53
+ [![Documentation Status](https://github.com/Leuven-Gravity-Institute/nullcal/actions/workflows/documentation.yml/badge.svg)](https://leuven-gravity-institute.github.io/nullcal)
54
+ [![codecov](https://codecov.io/gh/Leuven-Gravity-Institute/nullcal/branch/main/graph/badge.svg)](https://codecov.io/gh/Leuven-Gravity-Institute/nullcal)
55
+ [![PyPI Version](https://img.shields.io/pypi/v/nullcal)](https://pypi.org/project/nullcal/)
56
+ [![Python Versions](https://img.shields.io/pypi/pyversions/nullcal)](https://pypi.org/project/nullcal/)
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
58
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
59
+ [![DOI](https://zenodo.org/badge/ID.svg)](https://doi.org/DOI)
60
+
61
+ A Python package for constraining calibration errors of a closed-geometry
62
+ network of gravitational-wave detectors. It provides the null stream formalism
63
+ for noise-only data combinations and Bayesian recalibration likelihoods to
64
+ quantify detector miscalibration.
65
+
66
+ ## Features
67
+
68
+ - **Null stream construction** for closed-geometry detector networks
69
+ - **Calibration error constraint** via Bayesian recalibration likelihood
70
+ - **Time-frequency transforms** (Short-Time Fourier Transform, wavelet
71
+ transforms)
72
+ - **Clustering algorithms** for time-frequency map analysis
73
+ - **Modular architecture** with clean separation between null stream,
74
+ likelihood, calibration, immutable data, result, and utility layers
75
+
76
+ ## Installation
77
+
78
+ We recommend using `uv` to manage virtual environments for installing nullcal.
79
+
80
+ If you don't have `uv` installed, you can install it with pip. See the project
81
+ pages for more details:
82
+
83
+ - Install via pip: `pip install --upgrade pip && pip install uv`
84
+ - Project pages: [uv on PyPI](https://pypi.org/project/uv/) |
85
+ [uv on GitHub](https://github.com/astral-sh/uv)
86
+ - Full documentation and usage guide: [uv docs](https://docs.astral.sh/uv/)
87
+
88
+ **Note:** The package requires Python 3.12 or later and is built and tested
89
+ against Python 3.12–3.14. When creating a virtual environment with `uv`, specify
90
+ the Python version to ensure compatibility: `uv venv --python 3.12` (replace
91
+ `3.12` with your preferred supported version: 3.12, 3.13, or 3.14). This avoids
92
+ potential issues with unsupported Python versions.
93
+
94
+ ### From PyPI
95
+
96
+ ```bash
97
+ # Create a virtual environment (recommended with uv)
98
+ uv venv --python 3.12
99
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
100
+ uv pip install nullcal
101
+ ```
102
+
103
+ ### From Source
104
+
105
+ ```bash
106
+ git clone git@github.com:Leuven-Gravity-Institute/nullcal.git
107
+ cd nullcal
108
+ # Create a virtual environment (recommended with uv)
109
+ uv venv --python 3.12
110
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
111
+ uv sync --extra jax
112
+ ```
113
+
114
+ ## Quick Start
115
+
116
+ ```python
117
+ import numpy as np
118
+ from nullcal.data import InterferometerData
119
+ from nullcal.likelihood import RecalibrationLikelihood
120
+ from nullcal.null_stream.null_stream import NullStream
121
+ from nullcal.time_frequency_transform.wavelet_transforms import WaveletTransform
122
+
123
+ # Load arrays from your strain source. The loader also accepts detector objects
124
+ # exposing these fields via InterferometerData.from_interferometers(...).
125
+ duration = 4.0
126
+ sampling_frequency = 4096.0
127
+ frequency_array = np.fft.rfftfreq(int(duration * sampling_frequency), 1 / sampling_frequency)
128
+ data = InterferometerData(
129
+ psd=np.ones((3, frequency_array.size)),
130
+ strain=np.zeros((3, frequency_array.size), dtype=complex),
131
+ mask=np.broadcast_to(frequency_array >= 4.0, (3, frequency_array.size)).copy(),
132
+ frequency_array=frequency_array,
133
+ duration=duration,
134
+ sampling_frequency=sampling_frequency,
135
+ start_time=0.0,
136
+ name=("ET1", "ET2", "ET3"),
137
+ )
138
+
139
+ # Compute the null stream
140
+ wavelet_transform = WaveletTransform(
141
+ duration=duration, sampling_frequency=sampling_frequency, nx=4, frequency_resolution=4
142
+ )
143
+ time_frequency_filter = np.ones(wavelet_transform.shape, dtype=bool)
144
+ null_stream = NullStream(
145
+ interferometers=data,
146
+ time_frequency_transform=wavelet_transform,
147
+ time_frequency_filter=time_frequency_filter,
148
+ )
149
+ null_data = null_stream.compute_calibrated_frequency_domain_null_stream(calibration_factor=np.ones_like(data.strain))
150
+
151
+ # Set up a recalibration likelihood for calibration error constraints
152
+ likelihood = RecalibrationLikelihood(
153
+ interferometers=data,
154
+ knot_frequencies=np.geomspace(4.0, 2048.0, 10),
155
+ time_frequency_filter=time_frequency_filter,
156
+ wavelet_transform_frequency_resolution=4,
157
+ wavelet_transform_nx=4,
158
+ )
159
+
160
+ params = {
161
+ "amplitude": np.zeros((3, 10)),
162
+ "phase": np.zeros((3, 10)),
163
+ }
164
+ log_posterior = likelihood.logdensity_fn(params)
165
+ ```
166
+
167
+ ## Development
168
+
169
+ ### Pre-commit hooks
170
+
171
+ Install `prek` (a wrapper around `pre-commit`):
172
+
173
+ ```console
174
+ uv run prek install
175
+ ```
176
+
177
+ Run all hooks against all files:
178
+
179
+ ```console
180
+ uv run prek run --all-files
181
+ ```
182
+
183
+ ## Documentation
184
+
185
+ Full documentation is available at
186
+ [https://leuven-gravity-institute.github.io/nullcal/](https://leuven-gravity-institute.github.io/nullcal/).
187
+
188
+ ## Contributing
189
+
190
+ Contributions are welcome!
191
+
192
+ 1. Fork the repository
193
+ 2. Create a feature branch
194
+ 3. Make your changes
195
+ 4. Add tests
196
+ 5. Submit a pull request
197
+
198
+ ### Release Schedule
199
+
200
+ Releases follow a fixed schedule: every Tuesday at 00:00 UTC, unless an emergent
201
+ bugfix is required. This ensures predictable updates while allowing flexibility
202
+ for critical issues. Users can view upcoming changes in the draft release on the
203
+ [GitHub Releases page](https://github.com/Leuven-Gravity-Institute/nullcal/releases).
204
+
205
+ ## Testing
206
+
207
+ Run the test suite:
208
+
209
+ ```bash
210
+ uv run pytest
211
+ ```
212
+
213
+ ## License
214
+
215
+ This project is licensed under the **MIT License**. See the [LICENSE](LICENSE)
216
+ file for the full license text.
217
+
218
+ ## Support
219
+
220
+ For questions or issues, please open an issue on
221
+ [GitHub](https://github.com/Leuven-Gravity-Institute/nullcal/issues/new) or
222
+ contact the maintainers.
@@ -0,0 +1,42 @@
1
+ nullcal/__init__.py,sha256=nbJwrDKsfvCCDVWsV-b7K69ib6EIsBsAgrDugpRbaJs,269
2
+ nullcal/calibration.py,sha256=dW96gE44V2WAfh2Php2mN2tR6izOFCN4Pa1vbds91K0,7836
3
+ nullcal/data.py,sha256=8ib6wkWjsNVzkKSJIZ0apFTHSYHQ4ll45KTVg5iWu3M,3778
4
+ nullcal/sampler.py,sha256=j66SVbSV9LDRGA1141o8D4-u8q_IPt65Y2K9Y04UZbU,4356
5
+ nullcal/version.py,sha256=HdACpZyuwpg7fn8N-5nDf2JOTxzABBn1RkGLFe_DK60,142
6
+ nullcal/clustering/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ nullcal/clustering/base.py,sha256=_M2Iox3KubfpwpzfpvUFQbouEZQLDBXpeXcNSJ1PMkE,1172
8
+ nullcal/clustering/injection.py,sha256=2hORSfuXjoPzorxHKQ6fYwWRybDd8xSvbfNR5rGp6D0,2324
9
+ nullcal/clustering/precompute.py,sha256=gastImY1MHtRWXV6j44Ae6dUMqlDu6Gc8zkHxhDJjVk,1093
10
+ nullcal/clustering/single.py,sha256=G3Poul_QrRaucRrWLTMaiLTbxuY-H8cfZPVv3STaehs,8154
11
+ nullcal/clustering/time_frequency_map.py,sha256=-oCjC0fzjze5Xs0or7PqFDFESp0tBsXxwgCthGF-1is,1779
12
+ nullcal/likelihood/__init__.py,sha256=L7Q2BdTG8XkvZFakuXYpkrEANzgHMfNMxV3JeQvDM8U,180
13
+ nullcal/likelihood/recalibration_likelihood.py,sha256=UOfUyxqRIvXiY9yoQGE8hXExy62s1eDrmgMA39UidYQ,10823
14
+ nullcal/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ nullcal/metadata/yaml.py,sha256=8R5pSvAA2AyXK8AHMpk0g8fot8pBea8ISep5JewZfqc,655
16
+ nullcal/null_stream/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ nullcal/null_stream/calibration.py,sha256=CgR_dj95THcy0hdZ0xS_WvfiNwawq01okivaSha2PwY,1533
18
+ nullcal/null_stream/null_stream.py,sha256=oQ7HQ0UXnPyjglwbz2qqUeJrhW61T3qdUyeXn2HYF80,11991
19
+ nullcal/null_stream/projector.py,sha256=qujWOyDhEsZDkA07HidUgc1gkMgEhe499hkTNZl0nqc,1887
20
+ nullcal/null_stream/whiten.py,sha256=tbDNoc3CKpMQ-BLy7RzqSGTO-maTOK2p0vXcInoYEww,4287
21
+ nullcal/result/__init__.py,sha256=LL9RiEvSPU4hERph0hqBKIA8KFG24RwoXGoBKq4zAIs,75
22
+ nullcal/result/result.py,sha256=YCuhkmdcBcnOW08s8Na4UwXveX72yZtgSzgRe19kx8Y,1667
23
+ nullcal/result/utils.py,sha256=Y7-1e1FDnVaC1s0I-_FSy0x2B9Uev4u5CcEVZiSyuAc,299
24
+ nullcal/studies/__init__.py,sha256=ajvu4-ppzHoxNPGrQEOfa-b7MQ_sWsAF5Ox7nTCo4Zg,59
25
+ nullcal/studies/lwa_leakage.py,sha256=NH4pIS0A1GW4gwoB2kY40jzgmjWlCHZXrrxhqS7DEGM,21923
26
+ nullcal/studies/spline_resolution.py,sha256=Xx6DoqfKdtWr1pPiQuIXM3tNxv7zdLaEPudzDUpc9OQ,5905
27
+ nullcal/time_frequency_transform/README.md,sha256=KiLOcOK43Y6zHww3i0_wl7lalmq6T3cPhlbZv3dcPkY,335
28
+ nullcal/time_frequency_transform/__init__.py,sha256=3V8QF-S3HSFHE24GXyK7LsLNiJQChty5yVUuVm9xG7w,564
29
+ nullcal/time_frequency_transform/inverse_wavelet_freq_funcs.py,sha256=l__DXPyx5bwsNf2oUKPfcnVJwzCL1ELgq_36kP57030,1687
30
+ nullcal/time_frequency_transform/inverse_wavelet_time_funcs.py,sha256=yWPeh-RKGX52sqnelywCzxXvLg5FTAUaqPEc2ec2Y74,2116
31
+ nullcal/time_frequency_transform/stft.py,sha256=Kj7VlkSkPrCUt7N-BUKOTE_H5Eg58MfMCAe00m9C0iQ,1310
32
+ nullcal/time_frequency_transform/transform_freq_funcs.py,sha256=PwxAQtHCM_jZG7dhqDOPQhuYwMgAJxcWIxgZNHwWsIw,6280
33
+ nullcal/time_frequency_transform/transform_time_funcs.py,sha256=KeLbA-OX5d-a-MSYiyZ12koyva1OZD7fs-_7HalB4F0,2343
34
+ nullcal/time_frequency_transform/utils.py,sha256=WDt6hPji8tWncu2JX2USmxaKy8pSN3mvlRlQeSqIBdI,649
35
+ nullcal/time_frequency_transform/wavelet_transforms.py,sha256=co0b4br9oWIQpUCvx1R_88Zg-CUrkLWYCuQWy1OBJCk,9219
36
+ nullcal/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ nullcal/utils/log.py,sha256=cJ7jzH1IysyBf0truYWQQJLzFR2jRhGp1iQuOTBNyjc,2414
38
+ nullcal/utils/snr.py,sha256=U43mbgmMzY2bweEC2pyIPpvNdcA_dTe1mC3qr9hOU5A,1085
39
+ nullcal-0.2.0.dist-info/METADATA,sha256=GnMzFkroMLE9jgbABawS7ul4THyxyBp89hT4IOrwrxI,8584
40
+ nullcal-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
41
+ nullcal-0.2.0.dist-info/licenses/LICENSE,sha256=snOMIM58hahmLXUQ6hTKvk5-V-JuyalaCAP_NIuwdhQ,1071
42
+ nullcal-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Chun-Fung Wong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.