cattykit 0.1.1__tar.gz → 0.1.2__tar.gz

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 (31) hide show
  1. {cattykit-0.1.1 → cattykit-0.1.2}/PKG-INFO +8 -1
  2. {cattykit-0.1.1 → cattykit-0.1.2}/README.md +6 -0
  3. cattykit-0.1.2/cattykit/cli.py +28 -0
  4. cattykit-0.1.2/cattykit/experiments.py +602 -0
  5. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit/logging/__init__.py +2 -3
  6. cattykit-0.1.1/cattykit/logging/sqlite_logger.py → cattykit-0.1.2/cattykit/logging/cattycam_logger.py +258 -129
  7. cattykit-0.1.2/cattykit/logging/identifiers.py +209 -0
  8. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit/logging/model_logger.py +1 -4
  9. cattykit-0.1.2/cattykit/logging/null_logger.py +16 -0
  10. cattykit-0.1.2/cattykit/logging/print_logger.py +36 -0
  11. cattykit-0.1.2/cattykit/models/loading.py +314 -0
  12. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit.egg-info/PKG-INFO +8 -1
  13. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit.egg-info/SOURCES.txt +4 -2
  14. cattykit-0.1.2/cattykit.egg-info/entry_points.txt +2 -0
  15. cattykit-0.1.2/cattykit.egg-info/requires.txt +2 -0
  16. {cattykit-0.1.1 → cattykit-0.1.2}/pyproject.toml +5 -1
  17. cattykit-0.1.1/cattykit/experiments.py +0 -99
  18. cattykit-0.1.1/cattykit/logging/event.py +0 -32
  19. cattykit-0.1.1/cattykit/logging/null_logger.py +0 -13
  20. cattykit-0.1.1/cattykit/logging/print_logger.py +0 -24
  21. cattykit-0.1.1/cattykit/models/loading.py +0 -186
  22. cattykit-0.1.1/cattykit.egg-info/requires.txt +0 -1
  23. {cattykit-0.1.1 → cattykit-0.1.2}/LICENSE +0 -0
  24. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit/__init__.py +0 -0
  25. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit/models/__init__.py +0 -0
  26. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit/models/protocol.py +0 -0
  27. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit/models/registry.py +0 -0
  28. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit/py.typed +0 -0
  29. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit.egg-info/dependency_links.txt +0 -0
  30. {cattykit-0.1.1 → cattykit-0.1.2}/cattykit.egg-info/top_level.txt +0 -0
  31. {cattykit-0.1.1 → cattykit-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cattykit
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A framework for implementing artificial intelligence models using components from the cognitive models of the Fluid Analogies Research Group
5
5
  Author-email: George Alexander Wright <me@georgeaw.com>
6
6
  License: GNU AFFERO GENERAL PUBLIC LICENSE
@@ -672,6 +672,7 @@ Requires-Python: >=3.12
672
672
  Description-Content-Type: text/markdown
673
673
  License-File: LICENSE
674
674
  Requires-Dist: numpy>=2.4.3
675
+ Requires-Dist: pandas>=3.0.5
675
676
  Dynamic: license-file
676
677
 
677
678
  # Cattykit
@@ -707,6 +708,12 @@ print(cattykit.available_models())
707
708
  cattykit.install_model("path/to/model")
708
709
  ```
709
710
 
711
+ The same installation is available from the command line:
712
+
713
+ ```bash
714
+ cattykit install copycat --version 0.1.0
715
+ ```
716
+
710
717
  Once you have installed a model, you can ask it to solve a problem.
711
718
 
712
719
  ```python
@@ -31,6 +31,12 @@ print(cattykit.available_models())
31
31
  cattykit.install_model("path/to/model")
32
32
  ```
33
33
 
34
+ The same installation is available from the command line:
35
+
36
+ ```bash
37
+ cattykit install copycat --version 0.1.0
38
+ ```
39
+
34
40
  Once you have installed a model, you can ask it to solve a problem.
35
41
 
36
42
  ```python
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from collections.abc import Sequence
5
+
6
+ from .models import ModelInstallationError, ModelSourceError, install_model
7
+
8
+
9
+ def main(argv: Sequence[str] | None = None) -> None:
10
+ """Run the CattyKit command-line interface."""
11
+ parser = argparse.ArgumentParser(prog="cattykit")
12
+ commands = parser.add_subparsers(dest="command", required=True)
13
+
14
+ install_parser = commands.add_parser("install", help="Install a model.")
15
+ install_parser.add_argument(
16
+ "source", help="Official model name, HTTPS URL, or local path."
17
+ )
18
+ install_parser.add_argument(
19
+ "--version",
20
+ help="Official model version to install.",
21
+ )
22
+
23
+ args = parser.parse_args(argv)
24
+
25
+ try:
26
+ install_model(args.source, version=args.version)
27
+ except (ModelInstallationError, ModelSourceError) as error:
28
+ parser.error(str(error))
@@ -0,0 +1,602 @@
1
+ import sys
2
+ from math import erfc, exp, inf, isfinite, lgamma, log, sqrt
3
+ from pathlib import Path
4
+ from shutil import get_terminal_size
5
+
6
+ import pandas as pd
7
+
8
+ from .logging import CattycamLogger, NullLogger
9
+ from .models import load_model
10
+
11
+
12
+ SUMMARY_COLUMNS = [
13
+ "problem",
14
+ "solution",
15
+ "frequency",
16
+ "mean_codelets_run",
17
+ "codelets_standard_error",
18
+ "mean_temperature",
19
+ "temperature_standard_error",
20
+ ]
21
+
22
+
23
+ def run_experiment(
24
+ model_name: str,
25
+ problems: list[str],
26
+ iterations: int,
27
+ logging_db: str | Path | None = None,
28
+ verbose: bool = True,
29
+ ) -> pd.DataFrame:
30
+ """Run the model repeatedly on each problem.
31
+ The iteration count is used as the random seed given to the model, so the
32
+ same random seeds are used for each problem.
33
+ Returns one results DataFrame containing one row per solution plus a
34
+ totals row for each problem.
35
+ """
36
+ interactive = verbose and sys.stdout.isatty()
37
+ result_frames: list[pd.DataFrame] = []
38
+ logger = (
39
+ CattycamLogger(logging_db, model_name)
40
+ if logging_db is not None
41
+ else NullLogger(model_name)
42
+ )
43
+ try:
44
+ for problem_number, problem in enumerate(problems):
45
+ if interactive:
46
+ if problem_number:
47
+ print(_terminal_rule("="))
48
+ print(problem)
49
+ print(_terminal_rule("-"))
50
+ display = RunningAnswerDisplay()
51
+ runs: list[dict[str, str | float | int | None]] = []
52
+ for seed in range(iterations):
53
+ model = load_model(
54
+ model_name,
55
+ config={"seed": seed},
56
+ logger=logger,
57
+ )
58
+ try:
59
+ answer = model.solve(problem)
60
+ runs.append(
61
+ {
62
+ "solution": answer,
63
+ "codelets_run": model.coderack.number_of_codelets_run,
64
+ "temperature": model.temperature,
65
+ }
66
+ )
67
+ if interactive:
68
+ display.update(summarize_runs(runs, problem))
69
+ finally:
70
+ model.close()
71
+ result_frames.append(summarize_runs(runs, problem))
72
+ finally:
73
+ logger.close()
74
+ if not result_frames:
75
+ return pd.DataFrame(columns=SUMMARY_COLUMNS)
76
+ return pd.concat(result_frames, ignore_index=True)
77
+
78
+
79
+ def run_reproduction(
80
+ model_name: str,
81
+ gold_behaviour: dict[str, pd.DataFrame | list[dict]],
82
+ iterations: int,
83
+ verbose: bool = True,
84
+ output_file: str | None = None,
85
+ ) -> tuple[pd.DataFrame, dict[str, object]]:
86
+ """Compare current model behaviour with previously published behaviour.
87
+ ``gold_behaviour`` maps each problem to a summary table with the same
88
+ columns produced by ``summarize_runs``, except for ``problem``.
89
+ The returned table has paired observed and expected columns for every
90
+ expected solution and totals row. Differences are observed minus expected.
91
+ """
92
+ problems = list(gold_behaviour.keys())
93
+ observed = run_experiment(
94
+ model_name,
95
+ problems,
96
+ iterations,
97
+ verbose=False,
98
+ )
99
+ gold = _gold_behaviour_dataframe(gold_behaviour)
100
+ comparison_rows: list[dict[str, str | int | float | None]] = []
101
+ for problem in problems:
102
+ observed_problem = observed[observed["problem"] == problem]
103
+ gold_problem = gold[gold["problem"] == problem]
104
+ observed_solutions = observed_problem[observed_problem["solution"] != "Total"]
105
+ gold_solutions = gold_problem[gold_problem["solution"] != "Total"]
106
+ observed_distribution = dict(
107
+ zip(
108
+ observed_solutions["solution"],
109
+ observed_solutions["frequency"],
110
+ )
111
+ )
112
+ gold_distribution = dict(
113
+ zip(
114
+ gold_solutions["solution"],
115
+ gold_solutions["frequency"],
116
+ )
117
+ )
118
+ tv_distance = total_variation_distance(
119
+ observed_distribution,
120
+ gold_distribution,
121
+ )
122
+ observed_total = _total_row(
123
+ observed_problem,
124
+ problem,
125
+ "observed",
126
+ )
127
+ gold_total = _total_row(
128
+ gold_problem,
129
+ problem,
130
+ "gold",
131
+ )
132
+ codelets_z = z_statistic(
133
+ observed_mean=observed_total["mean_codelets_run"],
134
+ expected_mean=gold_total["mean_codelets_run"],
135
+ observed_standard_error=observed_total["codelets_standard_error"],
136
+ expected_standard_error=gold_total["codelets_standard_error"],
137
+ )
138
+ comparison_rows.append(
139
+ _comparison_row(
140
+ problem=problem,
141
+ solution="Total",
142
+ observed=observed_total,
143
+ expected=gold_total,
144
+ tv_distance=tv_distance,
145
+ codelets_z_stat=codelets_z,
146
+ )
147
+ )
148
+ for gold_row in gold_solutions.itertuples(index=False):
149
+ observed_match = observed_solutions[
150
+ observed_solutions["solution"] == gold_row.solution
151
+ ]
152
+ if observed_match.empty:
153
+ observed_row = None
154
+ temperature_z = None
155
+ else:
156
+ observed_row = observed_match.iloc[0]
157
+ temperature_z = z_statistic(
158
+ observed_mean=observed_row["mean_temperature"],
159
+ expected_mean=gold_row.mean_temperature,
160
+ observed_standard_error=observed_row["temperature_standard_error"],
161
+ expected_standard_error=(gold_row.temperature_standard_error),
162
+ )
163
+ comparison_rows.append(
164
+ _comparison_row(
165
+ problem=problem,
166
+ solution=gold_row.solution,
167
+ observed=observed_row,
168
+ expected=gold_row._asdict(),
169
+ temperature_z_stat=temperature_z,
170
+ )
171
+ )
172
+ comparison = pd.DataFrame(
173
+ comparison_rows,
174
+ columns=[
175
+ "problem",
176
+ "solution",
177
+ "observed_frequency",
178
+ "expected_frequency",
179
+ "frequency_difference",
180
+ "observed_mean_codelets_run",
181
+ "expected_mean_codelets_run",
182
+ "mean_codelets_run_difference",
183
+ "codelets_relative_error_magnitude",
184
+ "observed_codelets_standard_error",
185
+ "expected_codelets_standard_error",
186
+ "codelets_standard_error_difference",
187
+ "observed_mean_temperature",
188
+ "expected_mean_temperature",
189
+ "mean_temperature_difference",
190
+ "observed_temperature_standard_error",
191
+ "expected_temperature_standard_error",
192
+ "temperature_standard_error_difference",
193
+ "tv_distance",
194
+ "codelets_z_stat",
195
+ "temperature_z_stat",
196
+ ],
197
+ )
198
+ summary = _reproduction_summary(comparison)
199
+ if verbose:
200
+ print(
201
+ comparison.to_string(
202
+ index=False,
203
+ na_rep="-",
204
+ formatters={
205
+ "tv_distance": lambda x: f"{x:.4f}",
206
+ "codelets_z_stat": lambda x: f"{x:.2f}",
207
+ "codelets_relative_error_magnitude": lambda x: f"{x:.1%}",
208
+ "temperature_z_stat": lambda x: f"{x:.2f}",
209
+ },
210
+ )
211
+ )
212
+ for key, value in summary.items():
213
+ print(f"{key}: {value}")
214
+ if output_file:
215
+ comparison.to_csv(output_file, index=False)
216
+ return comparison, summary
217
+
218
+
219
+ def _comparison_row(
220
+ *,
221
+ problem: str,
222
+ solution: str,
223
+ observed: pd.Series | dict[str, object] | None,
224
+ expected: pd.Series | dict[str, object] | None,
225
+ tv_distance: float | None = None,
226
+ codelets_z_stat: float | None = None,
227
+ temperature_z_stat: float | None = None,
228
+ ) -> dict[str, str | float | int | None]:
229
+ """Build one observed-versus-expected comparison row."""
230
+ row: dict[str, str | float | int | None] = {
231
+ "problem": problem,
232
+ "solution": solution,
233
+ "tv_distance": tv_distance,
234
+ "codelets_z_stat": codelets_z_stat,
235
+ "temperature_z_stat": temperature_z_stat,
236
+ }
237
+ for field in (
238
+ "frequency",
239
+ "mean_codelets_run",
240
+ "codelets_standard_error",
241
+ "mean_temperature",
242
+ "temperature_standard_error",
243
+ ):
244
+ observed_value = _row_value(observed, field)
245
+ expected_value = _row_value(expected, field)
246
+ row[f"observed_{field}"] = observed_value
247
+ row[f"expected_{field}"] = expected_value
248
+ row[f"{field}_difference"] = _difference(
249
+ observed_value,
250
+ expected_value,
251
+ )
252
+ row["codelets_relative_error_magnitude"] = _relative_error_magnitude(
253
+ row["observed_mean_codelets_run"],
254
+ row["expected_mean_codelets_run"],
255
+ )
256
+ return row
257
+
258
+
259
+ def _row_value(
260
+ row: pd.Series | dict[str, object] | None,
261
+ field: str,
262
+ ) -> float | int | None:
263
+ if row is None:
264
+ return 0 if field == "frequency" else None
265
+ value = row.get(field)
266
+ return None if pd.isna(value) else value
267
+
268
+
269
+ def _difference(
270
+ observed: float | int | None,
271
+ expected: float | int | None,
272
+ ) -> float | int | None:
273
+ if observed is None or expected is None:
274
+ return None
275
+ return observed - expected
276
+
277
+
278
+ def _relative_error_magnitude(
279
+ observed: float | int | None,
280
+ expected: float | int | None,
281
+ ) -> float | None:
282
+ """Return the magnitude of error relative to the expected value."""
283
+ if observed is None or expected is None or expected == 0:
284
+ return None
285
+ return abs(observed - expected) / expected
286
+
287
+
288
+ def _gold_behaviour_dataframe(
289
+ gold_behaviour: dict[str, pd.DataFrame | list[dict]],
290
+ ) -> pd.DataFrame:
291
+ """Combine per-problem gold summaries into one DataFrame."""
292
+ frames: list[pd.DataFrame] = []
293
+ for problem, behaviour in gold_behaviour.items():
294
+ frame = (
295
+ behaviour.copy()
296
+ if isinstance(behaviour, pd.DataFrame)
297
+ else pd.DataFrame(behaviour)
298
+ )
299
+ frame.insert(0, "problem", problem)
300
+ frames.append(frame)
301
+ if not frames:
302
+ return pd.DataFrame(columns=SUMMARY_COLUMNS)
303
+ return pd.concat(frames, ignore_index=True)
304
+
305
+
306
+ def _total_row(
307
+ results: pd.DataFrame,
308
+ problem: str,
309
+ description: str,
310
+ ) -> pd.Series:
311
+ """Return the single totals row for a problem."""
312
+ total = results[results["solution"] == "Total"]
313
+ if len(total) != 1:
314
+ raise ValueError(
315
+ f"Expected exactly one Total row for {description} behaviour "
316
+ f"of {problem!r}, found {len(total)}."
317
+ )
318
+ return total.iloc[0]
319
+
320
+
321
+ def _reproduction_summary(comparison: pd.DataFrame) -> dict[str, object]:
322
+ """Calculate aggregate reproduction statistics from a comparison table."""
323
+ tv_distances = comparison["tv_distance"].dropna().tolist()
324
+ codelets_z_scores = comparison["codelets_z_stat"].dropna().tolist()
325
+ codelets_relative_error_magnitudes = comparison[
326
+ "codelets_relative_error_magnitude"
327
+ ].dropna().tolist()
328
+ temperature_z_scores = comparison.loc[
329
+ comparison["expected_frequency"] >= 10,
330
+ "temperature_z_stat",
331
+ ].dropna().tolist()
332
+ temperature_absolute_error_magnitudes = comparison.loc[
333
+ comparison["expected_frequency"] >= 10,
334
+ "mean_temperature_difference",
335
+ ].dropna().abs().tolist()
336
+ return {
337
+ "mean_total_variation_distance": _mean(tv_distances),
338
+ "max_total_variation_distance": max(tv_distances, default=0.0),
339
+ "mean_codelets_relative_error_magnitude": _mean(
340
+ codelets_relative_error_magnitudes,
341
+ ),
342
+ "max_codelets_relative_error_magnitude": max(
343
+ codelets_relative_error_magnitudes,
344
+ default=0.0,
345
+ ),
346
+ "codelets_relative_error_magnitude_quantiles": _deciles(
347
+ codelets_relative_error_magnitudes,
348
+ ),
349
+ "mean_temperature_absolute_error_magnitude": _mean(
350
+ temperature_absolute_error_magnitudes,
351
+ ),
352
+ "max_temperature_absolute_error_magnitude": max(
353
+ temperature_absolute_error_magnitudes,
354
+ default=0.0,
355
+ ),
356
+ "temperature_absolute_error_magnitude_quantiles": _deciles(
357
+ temperature_absolute_error_magnitudes,
358
+ ),
359
+ **_z_score_summary("codelets", codelets_z_scores),
360
+ **_z_score_summary("temperature", temperature_z_scores),
361
+ }
362
+
363
+
364
+ def _z_score_summary(prefix: str, z_scores: list[float]) -> dict[str, float]:
365
+ """Return chi-square and descriptive statistics for independent z scores."""
366
+ statistic = sum(z_score**2 for z_score in z_scores)
367
+ return {
368
+ f"{prefix}_chi_square_statistic": statistic,
369
+ f"{prefix}_chi_square_p_value": chi_square_survival_function(
370
+ statistic,
371
+ degrees_of_freedom=len(z_scores),
372
+ ),
373
+ f"{prefix}_rms_z_score": sqrt(statistic / len(z_scores)) if z_scores else 0.0,
374
+ f"{prefix}_max_absolute_z_score": max(
375
+ (abs(z_score) for z_score in z_scores),
376
+ default=0.0,
377
+ ),
378
+ }
379
+
380
+
381
+ def _mean(values: list[float]) -> float:
382
+ return sum(values) / len(values) if values else 0.0
383
+
384
+
385
+ def _deciles(values: list[float]) -> dict[int, float]:
386
+ """Return the 10th through 100th percentiles, keyed by percentile."""
387
+ if not values:
388
+ return {percentile: 0.0 for percentile in range(10, 101, 10)}
389
+ ordered = sorted(values)
390
+ result = {}
391
+ for percentile in range(10, 101, 10):
392
+ position = (len(ordered) - 1) * percentile / 100
393
+ lower = int(position)
394
+ upper = min(lower + 1, len(ordered) - 1)
395
+ fraction = position - lower
396
+ result[percentile] = ordered[lower] + fraction * (
397
+ ordered[upper] - ordered[lower]
398
+ )
399
+ return result
400
+
401
+
402
+ def chi_square_survival_function(
403
+ statistic: float,
404
+ degrees_of_freedom: int,
405
+ ) -> float:
406
+ """Return ``P(X >= statistic)`` for a chi-square random variable.
407
+
408
+ This is the regularized upper incomplete gamma function evaluated at
409
+ ``statistic / 2`` and avoids making SciPy a runtime dependency.
410
+ """
411
+ if degrees_of_freedom < 1:
412
+ return 1.0
413
+ if statistic <= 0:
414
+ return 1.0
415
+ if not isfinite(statistic):
416
+ return 0.0 if statistic == inf else float("nan")
417
+ return _regularized_gamma_q(degrees_of_freedom / 2, statistic / 2)
418
+
419
+
420
+ def _regularized_gamma_q(shape: float, value: float) -> float:
421
+ """Evaluate the regularized upper incomplete gamma function."""
422
+ epsilon = 1e-14
423
+ minimum = 1e-300
424
+ if value < shape + 1:
425
+ term = 1 / shape
426
+ total = term
427
+ for index in range(1, 10_000):
428
+ term *= value / (shape + index)
429
+ total += term
430
+ if abs(term) <= abs(total) * epsilon:
431
+ break
432
+ return 1 - total * exp(-value + shape * log(value) - lgamma(shape))
433
+
434
+ denominator = value + 1 - shape
435
+ continued_fraction = 1 / minimum
436
+ reciprocal = 1 / denominator
437
+ result = reciprocal
438
+ for index in range(1, 10_000):
439
+ numerator = -index * (index - shape)
440
+ denominator += 2
441
+ reciprocal = numerator * reciprocal + denominator
442
+ if abs(reciprocal) < minimum:
443
+ reciprocal = minimum
444
+ continued_fraction = denominator + numerator / continued_fraction
445
+ if abs(continued_fraction) < minimum:
446
+ continued_fraction = minimum
447
+ reciprocal = 1 / reciprocal
448
+ delta = reciprocal * continued_fraction
449
+ result *= delta
450
+ if abs(delta - 1) <= epsilon:
451
+ break
452
+ return exp(-value + shape * log(value) - lgamma(shape)) * result
453
+
454
+
455
+ def total_variation_distance(
456
+ observed: dict[str, int],
457
+ expected: dict[str, int],
458
+ ) -> float:
459
+ """Return total variation distance between two discrete distributions."""
460
+ observed_total = sum(observed.values())
461
+ expected_total = sum(expected.values())
462
+ if observed_total == 0 or expected_total == 0:
463
+ raise ValueError("Distributions must contain at least one observation.")
464
+ answers = observed.keys() | expected.keys()
465
+ return 0.5 * sum(
466
+ abs(
467
+ observed.get(answer, 0) / observed_total
468
+ - expected.get(answer, 0) / expected_total
469
+ )
470
+ for answer in answers
471
+ )
472
+
473
+
474
+ def z_statistic(
475
+ observed_mean: float,
476
+ expected_mean: float,
477
+ observed_standard_error: float,
478
+ expected_standard_error: float,
479
+ ) -> float:
480
+ """Return the z statistic for two means with independent errors."""
481
+ standard_error = sqrt(observed_standard_error**2 + expected_standard_error**2)
482
+ difference = observed_mean - expected_mean
483
+ if standard_error == 0:
484
+ if difference == 0:
485
+ return 0.0
486
+ return float("inf") if difference > 0 else float("-inf")
487
+ return difference / standard_error
488
+
489
+
490
+ def two_sided_p_value(z_score: float) -> float:
491
+ """Return the two-sided normal-distribution p value for a z statistic."""
492
+ return erfc(abs(z_score) / sqrt(2))
493
+
494
+
495
+ def _terminal_rule(character: str) -> str:
496
+ """Return a full-width terminal separator."""
497
+ return character * get_terminal_size(fallback=(80, 24)).columns
498
+
499
+
500
+ class RunningAnswerDisplay:
501
+ def __init__(self):
502
+ self.lines_printed = 0
503
+
504
+ def update(self, results: pd.DataFrame) -> None:
505
+ table = (
506
+ results.drop(columns="problem")
507
+ .rename(
508
+ columns={
509
+ "solution": "Solution",
510
+ "frequency": "Runs",
511
+ "mean_codelets_run": "Mean codelets",
512
+ "codelets_standard_error": "Codelets SE",
513
+ "mean_temperature": "Mean temp",
514
+ "temperature_standard_error": "Temp SE",
515
+ }
516
+ )
517
+ .to_string(
518
+ index=False,
519
+ formatters={
520
+ "Mean codelets": "{:.0f}".format,
521
+ "Codelets SE": "{:.1f}".format,
522
+ "Mean temp": "{:.2f}".format,
523
+ "Temp SE": "{:.4f}".format,
524
+ },
525
+ )
526
+ )
527
+ if self.lines_printed:
528
+ print(
529
+ f"\033[{self.lines_printed}A",
530
+ end="",
531
+ )
532
+ for _ in range(self.lines_printed):
533
+ print("\033[2K")
534
+ print(
535
+ f"\033[{self.lines_printed}A",
536
+ end="",
537
+ )
538
+ lines = table.splitlines()
539
+ for line in lines:
540
+ print(f"\033[2K{line}")
541
+ terminal_width = get_terminal_size(fallback=(80, 24)).columns
542
+ self.lines_printed = sum(
543
+ max(
544
+ 1,
545
+ (len(line) + terminal_width - 1) // terminal_width,
546
+ )
547
+ for line in lines
548
+ )
549
+
550
+
551
+ def summarize_runs(
552
+ runs: list[dict[str, str | float | int | None]],
553
+ problem: str,
554
+ ) -> pd.DataFrame:
555
+ """Summarize individual runs, including an overall totals row."""
556
+ data = pd.DataFrame(runs)
557
+ if data.empty:
558
+ return pd.DataFrame(columns=SUMMARY_COLUMNS)
559
+ grouped = data.groupby(
560
+ "solution",
561
+ sort=False,
562
+ )
563
+ summary = grouped.agg(
564
+ frequency=("solution", "size"),
565
+ mean_codelets_run=("codelets_run", "mean"),
566
+ codelets_standard_error=("codelets_run", "sem"),
567
+ mean_temperature=("temperature", "mean"),
568
+ temperature_standard_error=("temperature", "sem"),
569
+ ).reset_index()
570
+ summary = summary.sort_values(
571
+ "frequency",
572
+ ascending=False,
573
+ kind="stable",
574
+ )
575
+ error_columns = [
576
+ "codelets_standard_error",
577
+ "temperature_standard_error",
578
+ ]
579
+ summary[error_columns] = summary[error_columns].fillna(0.0)
580
+ totals = pd.DataFrame(
581
+ [
582
+ {
583
+ "solution": "Total",
584
+ "frequency": len(data),
585
+ "mean_codelets_run": (data["codelets_run"].mean()),
586
+ "codelets_standard_error": (data["codelets_run"].sem()),
587
+ "mean_temperature": (data["temperature"].mean()),
588
+ "temperature_standard_error": (data["temperature"].sem()),
589
+ }
590
+ ]
591
+ )
592
+ totals[error_columns] = totals[error_columns].fillna(0.0)
593
+ summary = pd.concat(
594
+ [summary, totals],
595
+ ignore_index=True,
596
+ )
597
+ summary.insert(
598
+ 0,
599
+ "problem",
600
+ problem,
601
+ )
602
+ return summary[SUMMARY_COLUMNS]
@@ -1,9 +1,8 @@
1
1
  """Structured logging for CattyKit model runs."""
2
2
 
3
- from .event import ModelEvent
3
+ from .cattycam_logger import CattycamLogger
4
4
  from .model_logger import ModelLogger
5
5
  from .null_logger import NullLogger
6
6
  from .print_logger import PrintLogger
7
- from .sqlite_logger import SQLiteLogger
8
7
 
9
- __all__ = ["ModelEvent", "ModelLogger", "NullLogger", "PrintLogger", "SQLiteLogger"]
8
+ __all__ = ["CattycamLogger", "ModelLogger", "NullLogger", "PrintLogger"]