Skip to content

Commit 0eae1a4

Browse files
Merge pull request #4441 from pybamm-team/telemetry
Add telemetry
2 parents 9ac0b5b + 1022c6b commit 0eae1a4

File tree

13 files changed

+389
-6
lines changed

13 files changed

+389
-6
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ input/*
4646

4747
# simulation outputs
4848
out/
49-
config.py
5049
matplotlibrc
5150
*.pickle
5251
*.sav

.lycheeignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ file:///home/runner/work/PyBaMM/PyBaMM/docs/source/user_guide/fundamentals/pybam
1515

1616
# Errors in docs/source/user_guide/index.md
1717
file:///home/runner/work/PyBaMM/PyBaMM/docs/source/user_guide/api_docs
18+
19+
# Telemetry
20+
https://us.i.posthog.com

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
- Adds an option "voltage as a state" that can be "false" (default) or "true". If "true" adds an explicit algebraic equation for the voltage. ([#4507](https://github.com/pybamm-team/PyBaMM/pull/4507))
77
- Improved `QuickPlot` accuracy for simulations with Hermite interpolation. ([#4483](https://github.com/pybamm-team/PyBaMM/pull/4483))
88
- Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464))
9+
- Added basic telemetry to record which functions are being run. See [Telemetry section in the User Guide](https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry) for more information. ([#4441](https://github.com/pybamm-team/PyBaMM/pull/4441))
910
- Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451))
1011
- Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415))
1112
- Added OpenMP parallelization to IDAKLU solver for lists of input parameters ([#4449](https://github.com/pybamm-team/PyBaMM/pull/4449))
12-
- Added phase-dependent particle options to LAM
13-
([#4369](https://github.com/pybamm-team/PyBaMM/pull/4369))
13+
- Added phase-dependent particle options to LAM ([#4369](https://github.com/pybamm-team/PyBaMM/pull/4369))
1414
- Added a lithium ion equivalent circuit model with split open circuit voltages for each electrode (`SplitOCVR`). ([#4330](https://github.com/pybamm-team/PyBaMM/pull/4330))
1515
- Added the `pybamm.DiscreteTimeSum` expression node to sum an expression over a sequence of data times, and accompanying `pybamm.DiscreteTimeData` class to store the data times and values ([#4501](https://github.com/pybamm-team/PyBaMM/pull/4501))
1616

conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,8 @@ def set_random_seed():
5151
@pytest.fixture(autouse=True)
5252
def set_debug_value():
5353
pybamm.settings.debug_mode = True
54+
55+
56+
@pytest.fixture(autouse=True)
57+
def disable_telemetry():
58+
pybamm.telemetry.disable()

docs/source/user_guide/index.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,12 @@ glob:
7373
../examples/notebooks/creating_models/5-half-cell-model.ipynb
7474
../examples/notebooks/creating_models/6-a-simple-SEI-model.ipynb
7575
```
76+
77+
# Telemetry
78+
79+
PyBaMM optionally collects anonymous usage data to help improve the library. This telemetry is opt-in and can be easily disabled. Here's what you need to know:
80+
81+
- **What is collected**: Basic usage information like PyBaMM version, Python version, and which functions are run.
82+
- **Why**: To understand how PyBaMM is used and prioritize development efforts.
83+
- **Opt-out**: To disable telemetry, set the environment variable `PYBAMM_DISABLE_TELEMETRY=true` (or any value other than `false`) or use `pybamm.telemetry.disable()` in your code.
84+
- **Privacy**: No personal information (name, email, etc) or sensitive information (parameter values, simulation results, etc) is ever collected.

docs/source/user_guide/installation/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ Package Minimum supp
7171
`typing-extensions <https://pypi.org/project/typing-extensions/>`__ 4.10.0
7272
`pandas <https://pypi.org/project/pandas/>`__ 1.5.0
7373
`pooch <https://www.fatiando.org/pooch/>`__ 1.8.1
74+
`posthog <https://posthog.com/>`__ 3.6.5
7475
=================================================================== ==========================
7576

7677
.. _install.optional_dependencies:

noxfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def set_iree_state():
4949
"IREE_INDEX_URL": os.getenv(
5050
"IREE_INDEX_URL", "https://iree.dev/pip-release-links.html"
5151
),
52+
"PYBAMM_DISABLE_TELEMETRY": "true",
5253
}
5354
VENV_DIR = Path("./venv").resolve()
5455

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies = [
4444
"typing-extensions>=4.10.0",
4545
"pandas>=1.5.0",
4646
"pooch>=1.8.1",
47+
"posthog",
4748
]
4849

4950
[project.urls]

src/pybamm/__init__.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,8 @@
194194
# Batch Study
195195
from .batch_study import BatchStudy
196196

197-
# Callbacks
198-
from . import callbacks
197+
# Callbacks, telemetry, config
198+
from . import callbacks, telemetry, config
199199

200200
# Pybamm Data manager using pooch
201201
from .pybamm_data import DataLoader
@@ -204,12 +204,14 @@
204204
import os
205205
import pathlib
206206
import sysconfig
207-
os.environ["CASADIPATH"] = str(pathlib.Path(sysconfig.get_path('purelib')) / 'casadi')
207+
208+
os.environ["CASADIPATH"] = str(pathlib.Path(sysconfig.get_path("purelib")) / "casadi")
208209

209210
__all__ = [
210211
"batch_study",
211212
"callbacks",
212213
"citations",
214+
"config",
213215
"discretisations",
214216
"doc_utils",
215217
"experiment",
@@ -225,8 +227,11 @@
225227
"simulation",
226228
"solvers",
227229
"spatial_methods",
230+
"telemetry",
228231
"type_definitions",
229232
"util",
230233
"version",
231234
"pybamm_data",
232235
]
236+
237+
pybamm.config.generate()

src/pybamm/config.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import uuid
2+
import os
3+
import platformdirs
4+
from pathlib import Path
5+
import pybamm
6+
import sys
7+
import threading
8+
import time
9+
10+
11+
def is_running_tests(): # pragma: no cover
12+
"""
13+
Detect if the code is being run as part of a test suite or building docs with Sphinx.
14+
15+
Returns:
16+
bool: True if running tests or building docs, False otherwise.
17+
"""
18+
# Check if pytest or unittest is running
19+
if any(
20+
test_module in sys.modules for test_module in ["pytest", "unittest", "nose"]
21+
):
22+
return True
23+
24+
# Check for GitHub Actions environment variable
25+
if "GITHUB_ACTIONS" in os.environ:
26+
return True
27+
28+
# Check for other common CI environment variables
29+
ci_env_vars = ["CI", "TRAVIS", "CIRCLECI", "JENKINS_URL", "GITLAB_CI"]
30+
if any(var in os.environ for var in ci_env_vars):
31+
return True
32+
33+
# Check for common test runner names in command-line arguments
34+
test_runners = ["pytest", "unittest", "nose", "trial", "nox", "tox"]
35+
if any(runner in arg.lower() for arg in sys.argv for runner in test_runners):
36+
return True
37+
38+
# Check if building docs with Sphinx
39+
if any(mod == "sphinx" or mod.startswith("sphinx.") for mod in sys.modules):
40+
print(
41+
f"Found Sphinx module: {[mod for mod in sys.modules if mod.startswith('sphinx')]}"
42+
)
43+
return True
44+
45+
return False
46+
47+
48+
def ask_user_opt_in(timeout=10):
49+
"""
50+
Ask the user if they want to opt in to telemetry.
51+
52+
Parameters
53+
----------
54+
timeout : float, optional
55+
The timeout for the user to respond to the prompt. Default is 10 seconds.
56+
57+
Returns
58+
-------
59+
bool
60+
True if the user opts in, False otherwise.
61+
"""
62+
print(
63+
"PyBaMM can collect usage data and send it to the PyBaMM team to "
64+
"help us improve the software.\n"
65+
"We do not collect any sensitive information such as models, parameters, "
66+
"or simulation results - only information on which parts of the code are "
67+
"being used and how frequently.\n"
68+
"This is entirely optional and does not impact the functionality of PyBaMM.\n"
69+
"For more information, see https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry"
70+
)
71+
72+
def get_input(): # pragma: no cover
73+
try:
74+
user_input = (
75+
input("Do you want to enable telemetry? (Y/n): ").strip().lower()
76+
)
77+
answer.append(user_input)
78+
except Exception:
79+
# Handle any input errors
80+
pass
81+
82+
time_start = time.time()
83+
84+
while True:
85+
if time.time() - time_start > timeout:
86+
print("\nTimeout reached. Defaulting to not enabling telemetry.")
87+
return False
88+
89+
answer = []
90+
# Create and start input thread
91+
input_thread = threading.Thread(target=get_input)
92+
input_thread.daemon = True
93+
input_thread.start()
94+
95+
# Wait for either timeout or input
96+
input_thread.join(timeout)
97+
98+
if answer:
99+
if answer[0] in ["yes", "y", ""]:
100+
print("\nTelemetry enabled.\n")
101+
return True
102+
elif answer[0] in ["no", "n"]:
103+
print("\nTelemetry disabled.\n")
104+
return False
105+
else:
106+
print("\nInvalid input. Please enter 'yes/y' for yes or 'no/n' for no.")
107+
else:
108+
print("\nTimeout reached. Defaulting to not enabling telemetry.")
109+
return False
110+
111+
112+
def generate():
113+
if is_running_tests():
114+
return
115+
116+
# Check if the config file already exists
117+
if read() is not None:
118+
return
119+
120+
# Ask the user if they want to opt in to telemetry
121+
opt_in = ask_user_opt_in()
122+
config_file = Path(platformdirs.user_config_dir("pybamm")) / "config.yml"
123+
write_uuid_to_file(config_file, opt_in)
124+
125+
if opt_in:
126+
pybamm.telemetry.capture("user-opted-in")
127+
128+
129+
def read():
130+
config_file = Path(platformdirs.user_config_dir("pybamm")) / "config.yml"
131+
return read_uuid_from_file(config_file)
132+
133+
134+
def write_uuid_to_file(config_file, opt_in):
135+
# Create the directory if it doesn't exist
136+
config_file.parent.mkdir(parents=True, exist_ok=True)
137+
138+
# Write the UUID to the config file in YAML format
139+
with open(config_file, "w") as f:
140+
f.write("pybamm:\n")
141+
f.write(f" enable_telemetry: {opt_in}\n")
142+
if opt_in:
143+
unique_id = uuid.uuid4()
144+
f.write(f" uuid: {unique_id}\n")
145+
146+
147+
def read_uuid_from_file(config_file):
148+
# Check if the config file exists
149+
if not config_file.exists():
150+
return None
151+
152+
# Read the UUID from the config file
153+
with open(config_file) as f:
154+
content = f.read().strip()
155+
156+
# Extract the UUID using YAML parsing
157+
try:
158+
import yaml
159+
160+
config = yaml.safe_load(content)
161+
return config["pybamm"]
162+
except (yaml.YAMLError, ValueError):
163+
return None

0 commit comments

Comments
 (0)