hypothesis-helm 0.1.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 (322) hide show
  1. hypothesis_helm/__init__.py +38 -0
  2. hypothesis_helm/analysis/__init__.py +3 -0
  3. hypothesis_helm/analysis/cli.py +117 -0
  4. hypothesis_helm/analysis/report.py +172 -0
  5. hypothesis_helm/analysis/sensitivity.py +268 -0
  6. hypothesis_helm/benchmarking/__init__.py +3 -0
  7. hypothesis_helm/benchmarking/analysis/__init__.py +3 -0
  8. hypothesis_helm/benchmarking/analysis/calibration_matrix.py +230 -0
  9. hypothesis_helm/benchmarking/analysis/expansion_cost.py +51 -0
  10. hypothesis_helm/benchmarking/analysis/pca.py +186 -0
  11. hypothesis_helm/benchmarking/analysis/quadratic.py +98 -0
  12. hypothesis_helm/benchmarking/analysis/selection.py +112 -0
  13. hypothesis_helm/benchmarking/analysis/symbolic.py +94 -0
  14. hypothesis_helm/benchmarking/assets/chart/Chart.yaml +4 -0
  15. hypothesis_helm/benchmarking/assets/chart/benchmark-parameters.yaml +28 -0
  16. hypothesis_helm/benchmarking/assets/chart/benchmark.json +93 -0
  17. hypothesis_helm/benchmarking/assets/chart/templates/configmap.yaml +6 -0
  18. hypothesis_helm/benchmarking/assets/chart/templates/stress.yaml +115 -0
  19. hypothesis_helm/benchmarking/assets/chart/topology-parameters.yaml +7 -0
  20. hypothesis_helm/benchmarking/assets/chart/values.schema.json +104 -0
  21. hypothesis_helm/benchmarking/assets/chart/values.yaml +12 -0
  22. hypothesis_helm/benchmarking/assets/fixture/standard.yaml +21 -0
  23. hypothesis_helm/benchmarking/assets/fixture/topology.yaml +21 -0
  24. hypothesis_helm/benchmarking/charts/__init__.py +3 -0
  25. hypothesis_helm/benchmarking/charts/error_surface.py +153 -0
  26. hypothesis_helm/benchmarking/charts/faults.py +145 -0
  27. hypothesis_helm/benchmarking/charts/fixture.py +176 -0
  28. hypothesis_helm/benchmarking/charts/generator.py +517 -0
  29. hypothesis_helm/benchmarking/charts/manifests.py +17 -0
  30. hypothesis_helm/benchmarking/charts/mixtures.py +93 -0
  31. hypothesis_helm/benchmarking/charts/names.py +66 -0
  32. hypothesis_helm/benchmarking/charts/parameters.py +57 -0
  33. hypothesis_helm/benchmarking/charts/shape.py +63 -0
  34. hypothesis_helm/benchmarking/charts/stress.py +301 -0
  35. hypothesis_helm/benchmarking/charts/structural_sparsity.py +158 -0
  36. hypothesis_helm/benchmarking/charts/structures.py +290 -0
  37. hypothesis_helm/benchmarking/charts/topology.py +183 -0
  38. hypothesis_helm/benchmarking/charts/workload.py +192 -0
  39. hypothesis_helm/benchmarking/cli.py +128 -0
  40. hypothesis_helm/benchmarking/execution/__init__.py +3 -0
  41. hypothesis_helm/benchmarking/execution/cancellation.py +90 -0
  42. hypothesis_helm/benchmarking/execution/profiling.py +202 -0
  43. hypothesis_helm/benchmarking/execution/provenance.py +21 -0
  44. hypothesis_helm/benchmarking/execution/runner.py +424 -0
  45. hypothesis_helm/benchmarking/execution/shell.py +33 -0
  46. hypothesis_helm/benchmarking/refresh/__init__.py +3 -0
  47. hypothesis_helm/benchmarking/refresh/ci.py +130 -0
  48. hypothesis_helm/benchmarking/refresh/cli.py +170 -0
  49. hypothesis_helm/benchmarking/refresh/plan.py +103 -0
  50. hypothesis_helm/benchmarking/refresh/recipes/catalog-topologies.py +211 -0
  51. hypothesis_helm/benchmarking/refresh/recipes/chart-topology.sh +38 -0
  52. hypothesis_helm/benchmarking/refresh/recipes/discovery-tables.py +71 -0
  53. hypothesis_helm/benchmarking/refresh/recipes/finalize-repository.py +339 -0
  54. hypothesis_helm/benchmarking/refresh/recipes/initialize.py +146 -0
  55. hypothesis_helm/benchmarking/refresh/recipes/operations.sh +77 -0
  56. hypothesis_helm/benchmarking/refresh/recipes/plan-topology-retries.py +47 -0
  57. hypothesis_helm/benchmarking/refresh/recipes/polish-sparsity.py +17 -0
  58. hypothesis_helm/benchmarking/refresh/recipes/prepare-fixtures.py +23 -0
  59. hypothesis_helm/benchmarking/refresh/recipes/prepare-topologies.py +37 -0
  60. hypothesis_helm/benchmarking/refresh/recipes/publish-flamegraphs.py +62 -0
  61. hypothesis_helm/benchmarking/refresh/recipes/publish.py +41 -0
  62. hypothesis_helm/benchmarking/refresh/recipes/repository-chart.sh +10 -0
  63. hypothesis_helm/benchmarking/refresh/recipes/repository-run.sh +12 -0
  64. hypothesis_helm/benchmarking/refresh/recipes/retry-topologies.sh +13 -0
  65. hypothesis_helm/benchmarking/refresh/recipes/run-topologies.sh +14 -0
  66. hypothesis_helm/benchmarking/refresh/recipes/sparsity-tables.py +94 -0
  67. hypothesis_helm/benchmarking/refresh/recipes/studies.sh +69 -0
  68. hypothesis_helm/benchmarking/refresh/recipes/update-documentation.py +99 -0
  69. hypothesis_helm/benchmarking/refresh/recipes/verify-measurements.py +165 -0
  70. hypothesis_helm/benchmarking/refresh/recipes/verify-publication.py +95 -0
  71. hypothesis_helm/benchmarking/refresh/recipes/verify-topologies.py +68 -0
  72. hypothesis_helm/benchmarking/reporting/__init__.py +3 -0
  73. hypothesis_helm/benchmarking/reporting/complexity_sweep.py +91 -0
  74. hypothesis_helm/benchmarking/reporting/depth.py +162 -0
  75. hypothesis_helm/benchmarking/reporting/descriptions.py +203 -0
  76. hypothesis_helm/benchmarking/reporting/error_highlight.py +87 -0
  77. hypothesis_helm/benchmarking/reporting/error_surface.py +259 -0
  78. hypothesis_helm/benchmarking/reporting/expansion.py +222 -0
  79. hypothesis_helm/benchmarking/reporting/flamegraph.py +232 -0
  80. hypothesis_helm/benchmarking/reporting/labels.py +22 -0
  81. hypothesis_helm/benchmarking/reporting/matrix.py +179 -0
  82. hypothesis_helm/benchmarking/reporting/nesting.py +278 -0
  83. hypothesis_helm/benchmarking/reporting/pca.py +265 -0
  84. hypothesis_helm/benchmarking/reporting/plots.py +452 -0
  85. hypothesis_helm/benchmarking/reporting/polynomial_surface.py +104 -0
  86. hypothesis_helm/benchmarking/reporting/progress.py +149 -0
  87. hypothesis_helm/benchmarking/reporting/response_surface.py +164 -0
  88. hypothesis_helm/benchmarking/reporting/structural_sparsity.py +142 -0
  89. hypothesis_helm/benchmarking/reporting/symbolic.py +119 -0
  90. hypothesis_helm/benchmarking/reporting/variation.py +94 -0
  91. hypothesis_helm/benchmarking/scripts/shards.sh +85 -0
  92. hypothesis_helm/benchmarking/scripts/smoke.sh +39 -0
  93. hypothesis_helm/benchmarking/studies/__init__.py +3 -0
  94. hypothesis_helm/benchmarking/studies/calibration.py +391 -0
  95. hypothesis_helm/benchmarking/studies/discovery.py +249 -0
  96. hypothesis_helm/benchmarking/studies/error_surface.py +374 -0
  97. hypothesis_helm/benchmarking/studies/expansion.py +249 -0
  98. hypothesis_helm/benchmarking/studies/filtering.py +362 -0
  99. hypothesis_helm/benchmarking/studies/matrix.py +387 -0
  100. hypothesis_helm/benchmarking/studies/nesting.py +208 -0
  101. hypothesis_helm/benchmarking/studies/pca.py +342 -0
  102. hypothesis_helm/benchmarking/studies/performance.py +392 -0
  103. hypothesis_helm/benchmarking/studies/polynomial_surface.py +87 -0
  104. hypothesis_helm/benchmarking/studies/response_surface.py +275 -0
  105. hypothesis_helm/benchmarking/studies/sampling.py +266 -0
  106. hypothesis_helm/benchmarking/studies/sensitivity.py +154 -0
  107. hypothesis_helm/benchmarking/studies/sparsity.py +268 -0
  108. hypothesis_helm/benchmarking/studies/stress.py +235 -0
  109. hypothesis_helm/benchmarking/studies/structural_sparsity.py +203 -0
  110. hypothesis_helm/benchmarking/studies/structure_depth.py +186 -0
  111. hypothesis_helm/benchmarking/studies/symbolic_surface.py +235 -0
  112. hypothesis_helm/benchmarking/studies/topology.py +271 -0
  113. hypothesis_helm/charts/__init__.py +3 -0
  114. hypothesis_helm/charts/audit.py +82 -0
  115. hypothesis_helm/charts/cache.py +133 -0
  116. hypothesis_helm/charts/candidates.py +215 -0
  117. hypothesis_helm/charts/changes.py +147 -0
  118. hypothesis_helm/charts/exhaustive.py +170 -0
  119. hypothesis_helm/charts/generate.py +522 -0
  120. hypothesis_helm/charts/generated.py +309 -0
  121. hypothesis_helm/charts/model.py +200 -0
  122. hypothesis_helm/charts/paths.py +374 -0
  123. hypothesis_helm/charts/planning.py +445 -0
  124. hypothesis_helm/charts/presence.py +32 -0
  125. hypothesis_helm/charts/prioritized.py +221 -0
  126. hypothesis_helm/charts/registry.py +236 -0
  127. hypothesis_helm/charts/rendering.py +204 -0
  128. hypothesis_helm/charts/repository.py +152 -0
  129. hypothesis_helm/charts/runner.py +759 -0
  130. hypothesis_helm/charts/scan.py +599 -0
  131. hypothesis_helm/charts/templates.py +323 -0
  132. hypothesis_helm/charts/tpl.py +86 -0
  133. hypothesis_helm/charts/yamlio.py +116 -0
  134. hypothesis_helm/cli.py +936 -0
  135. hypothesis_helm/compiler/__init__.py +3 -0
  136. hypothesis_helm/compiler/asts/__init__.py +3 -0
  137. hypothesis_helm/compiler/asts/actions.py +76 -0
  138. hypothesis_helm/compiler/asts/conditions.py +88 -0
  139. hypothesis_helm/compiler/asts/contracts.py +426 -0
  140. hypothesis_helm/compiler/asts/dependencies.py +47 -0
  141. hypothesis_helm/compiler/asts/lattice.py +168 -0
  142. hypothesis_helm/compiler/asts/lexing.py +88 -0
  143. hypothesis_helm/compiler/asts/templates.py +267 -0
  144. hypothesis_helm/compiler/complexity.py +59 -0
  145. hypothesis_helm/compiler/constants.py +89 -0
  146. hypothesis_helm/compiler/passes/__init__.py +3 -0
  147. hypothesis_helm/compiler/passes/branches.py +83 -0
  148. hypothesis_helm/compiler/passes/complexity.py +413 -0
  149. hypothesis_helm/compiler/passes/dependencies.py +437 -0
  150. hypothesis_helm/compiler/passes/expansion.py +92 -0
  151. hypothesis_helm/compiler/passes/exports.py +77 -0
  152. hypothesis_helm/compiler/passes/graph.py +243 -0
  153. hypothesis_helm/compiler/passes/inputs.py +384 -0
  154. hypothesis_helm/compiler/passes/minimum.py +276 -0
  155. hypothesis_helm/compiler/passes/pruning.py +534 -0
  156. hypothesis_helm/compiler/passes/rejections.py +192 -0
  157. hypothesis_helm/compiler/passes/sampling.py +99 -0
  158. hypothesis_helm/compiler/passes/topology.py +103 -0
  159. hypothesis_helm/execution/__init__.py +3 -0
  160. hypothesis_helm/execution/aggressive.py +237 -0
  161. hypothesis_helm/execution/cache.py +194 -0
  162. hypothesis_helm/execution/calibration.json +4876 -0
  163. hypothesis_helm/execution/environment.py +29 -0
  164. hypothesis_helm/execution/estimate.py +208 -0
  165. hypothesis_helm/execution/feedback.py +146 -0
  166. hypothesis_helm/execution/parallel.py +273 -0
  167. hypothesis_helm/execution/path_queue.py +213 -0
  168. hypothesis_helm/execution/processes.py +205 -0
  169. hypothesis_helm/execution/render_hashes.py +194 -0
  170. hypothesis_helm/execution/sampling.py +119 -0
  171. hypothesis_helm/execution/signals.py +120 -0
  172. hypothesis_helm/execution/structure.py +163 -0
  173. hypothesis_helm/execution/suite.py +307 -0
  174. hypothesis_helm/execution/traversal.py +156 -0
  175. hypothesis_helm/findings/__init__.py +3 -0
  176. hypothesis_helm/findings/catalog.py +219 -0
  177. hypothesis_helm/findings/generator.py +159 -0
  178. hypothesis_helm/integrations/__init__.py +3 -0
  179. hypothesis_helm/integrations/github_action.py +120 -0
  180. hypothesis_helm/integrations/github_action.sh +38 -0
  181. hypothesis_helm/integrations/kubesec.py +218 -0
  182. hypothesis_helm/integrations/minimal_values.sh +47 -0
  183. hypothesis_helm/integrations/sharding.py +201 -0
  184. hypothesis_helm/reporting/__init__.py +3 -0
  185. hypothesis_helm/reporting/assets/logo.png +0 -0
  186. hypothesis_helm/reporting/budget.py +80 -0
  187. hypothesis_helm/reporting/changes.py +155 -0
  188. hypothesis_helm/reporting/contents.py +123 -0
  189. hypothesis_helm/reporting/display.py +48 -0
  190. hypothesis_helm/reporting/errors.py +217 -0
  191. hypothesis_helm/reporting/links.py +110 -0
  192. hypothesis_helm/reporting/output.py +38 -0
  193. hypothesis_helm/reporting/pdf.py +107 -0
  194. hypothesis_helm/reporting/permutations.py +284 -0
  195. hypothesis_helm/reporting/progress.py +229 -0
  196. hypothesis_helm/reporting/progressive.py +380 -0
  197. hypothesis_helm/reporting/repository.py +286 -0
  198. hypothesis_helm/reporting/reproductions.py +190 -0
  199. hypothesis_helm/reporting/shards.py +233 -0
  200. hypothesis_helm/rules.py +151 -0
  201. hypothesis_helm/schemas/__init__.py +3 -0
  202. hypothesis_helm/schemas/combinations.py +277 -0
  203. hypothesis_helm/schemas/conformity.py +215 -0
  204. hypothesis_helm/schemas/contracts.py +164 -0
  205. hypothesis_helm/schemas/factors.py +95 -0
  206. hypothesis_helm/schemas/finite.py +169 -0
  207. hypothesis_helm/schemas/groups.py +144 -0
  208. hypothesis_helm/schemas/model.py +431 -0
  209. hypothesis_helm/schemas/paths.py +169 -0
  210. hypothesis_helm/schemas/priority.py +128 -0
  211. hypothesis_helm/schemas/replay.py +150 -0
  212. hypothesis_helm/tests/__init__.py +3 -0
  213. hypothesis_helm/tests/conftest.py +47 -0
  214. hypothesis_helm/tests/test_aggregate.py +149 -0
  215. hypothesis_helm/tests/test_aggressive.py +327 -0
  216. hypothesis_helm/tests/test_benchmark_package.py +132 -0
  217. hypothesis_helm/tests/test_benchmark_progress.py +138 -0
  218. hypothesis_helm/tests/test_benchmarks.py +312 -0
  219. hypothesis_helm/tests/test_binary_cache.py +240 -0
  220. hypothesis_helm/tests/test_cache.py +238 -0
  221. hypothesis_helm/tests/test_calibration_matrix.py +306 -0
  222. hypothesis_helm/tests/test_changes.py +239 -0
  223. hypothesis_helm/tests/test_chart_changes.py +281 -0
  224. hypothesis_helm/tests/test_ci.py +556 -0
  225. hypothesis_helm/tests/test_combinations.py +194 -0
  226. hypothesis_helm/tests/test_complexity.py +336 -0
  227. hypothesis_helm/tests/test_conformity.py +237 -0
  228. hypothesis_helm/tests/test_constants.py +153 -0
  229. hypothesis_helm/tests/test_contents.py +107 -0
  230. hypothesis_helm/tests/test_dependencies.py +434 -0
  231. hypothesis_helm/tests/test_discovery.py +90 -0
  232. hypothesis_helm/tests/test_display.py +120 -0
  233. hypothesis_helm/tests/test_distinct_configurations.py +99 -0
  234. hypothesis_helm/tests/test_environment.py +65 -0
  235. hypothesis_helm/tests/test_error_highlight.py +56 -0
  236. hypothesis_helm/tests/test_error_surface.py +257 -0
  237. hypothesis_helm/tests/test_errors.py +332 -0
  238. hypothesis_helm/tests/test_estimate.py +128 -0
  239. hypothesis_helm/tests/test_expansion.py +493 -0
  240. hypothesis_helm/tests/test_feedback.py +79 -0
  241. hypothesis_helm/tests/test_filtering_load.py +85 -0
  242. hypothesis_helm/tests/test_findings.py +164 -0
  243. hypothesis_helm/tests/test_finite.py +57 -0
  244. hypothesis_helm/tests/test_fixture.py +248 -0
  245. hypothesis_helm/tests/test_generate.py +349 -0
  246. hypothesis_helm/tests/test_group_coverage.py +240 -0
  247. hypothesis_helm/tests/test_input_inventory.py +430 -0
  248. hypothesis_helm/tests/test_kubesec.py +149 -0
  249. hypothesis_helm/tests/test_lattice.py +240 -0
  250. hypothesis_helm/tests/test_local_shards.py +72 -0
  251. hypothesis_helm/tests/test_matrix.py +197 -0
  252. hypothesis_helm/tests/test_mixtures.py +138 -0
  253. hypothesis_helm/tests/test_operations.py +521 -0
  254. hypothesis_helm/tests/test_parallel_exhaustive.py +266 -0
  255. hypothesis_helm/tests/test_path_scan.py +222 -0
  256. hypothesis_helm/tests/test_path_workers.py +290 -0
  257. hypothesis_helm/tests/test_pca.py +126 -0
  258. hypothesis_helm/tests/test_permutation_statistics.py +190 -0
  259. hypothesis_helm/tests/test_plot_descriptions.py +55 -0
  260. hypothesis_helm/tests/test_plot_variation.py +108 -0
  261. hypothesis_helm/tests/test_priority.py +314 -0
  262. hypothesis_helm/tests/test_profiling.py +203 -0
  263. hypothesis_helm/tests/test_progressive.py +153 -0
  264. hypothesis_helm/tests/test_pruning.py +464 -0
  265. hypothesis_helm/tests/test_quadratic.py +162 -0
  266. hypothesis_helm/tests/test_refresh.py +771 -0
  267. hypothesis_helm/tests/test_refresh_ci.py +117 -0
  268. hypothesis_helm/tests/test_registry.py +349 -0
  269. hypothesis_helm/tests/test_rejections.py +306 -0
  270. hypothesis_helm/tests/test_release_version.py +59 -0
  271. hypothesis_helm/tests/test_remote_shards.py +78 -0
  272. hypothesis_helm/tests/test_render_hashes.py +197 -0
  273. hypothesis_helm/tests/test_replay.py +174 -0
  274. hypothesis_helm/tests/test_repository.py +243 -0
  275. hypothesis_helm/tests/test_reproductions.py +218 -0
  276. hypothesis_helm/tests/test_rules.py +256 -0
  277. hypothesis_helm/tests/test_runner.py +266 -0
  278. hypothesis_helm/tests/test_sampling.py +171 -0
  279. hypothesis_helm/tests/test_scan.py +858 -0
  280. hypothesis_helm/tests/test_sensitivity.py +262 -0
  281. hypothesis_helm/tests/test_sharding.py +399 -0
  282. hypothesis_helm/tests/test_shutdown.py +804 -0
  283. hypothesis_helm/tests/test_sparsity.py +47 -0
  284. hypothesis_helm/tests/test_strict.py +142 -0
  285. hypothesis_helm/tests/test_structural_sparsity.py +108 -0
  286. hypothesis_helm/tests/test_structure.py +139 -0
  287. hypothesis_helm/tests/test_suite.py +382 -0
  288. hypothesis_helm/tests/test_symbolic.py +111 -0
  289. hypothesis_helm/tests/test_templates.py +268 -0
  290. hypothesis_helm/tests/test_time_budget.py +197 -0
  291. hypothesis_helm/tests/test_topology_plotting.py +113 -0
  292. hypothesis_helm/tests/test_traversal.py +167 -0
  293. hypothesis_helm/tests/test_trim.py +165 -0
  294. hypothesis_helm/tests/test_values_model.py +183 -0
  295. hypothesis_helm/tests/test_verified_exports.py +395 -0
  296. hypothesis_helm/tests/test_workbalance.py +875 -0
  297. hypothesis_helm/tests/test_workbalance_gates.py +121 -0
  298. hypothesis_helm-0.1.0.dist-info/LICENSE +674 -0
  299. hypothesis_helm-0.1.0.dist-info/METADATA +290 -0
  300. hypothesis_helm-0.1.0.dist-info/RECORD +322 -0
  301. hypothesis_helm-0.1.0.dist-info/WHEEL +4 -0
  302. hypothesis_helm-0.1.0.dist-info/entry_points.txt +12 -0
  303. workbalance/README.md +317 -0
  304. workbalance/__init__.py +11 -0
  305. workbalance/checkpoints.py +77 -0
  306. workbalance/docs/images/pipeline-expanded.png +0 -0
  307. workbalance/docs/images/pipeline-fork-join.png +0 -0
  308. workbalance/docs/images/pipeline-initial.png +0 -0
  309. workbalance/example.py +99 -0
  310. workbalance/feedback.py +138 -0
  311. workbalance/graph.py +209 -0
  312. workbalance/plotting.py +56 -0
  313. workbalance/policy.py +192 -0
  314. workbalance/py.typed +0 -0
  315. workbalance/scheduler.py +604 -0
  316. workgraph/__init__.py +16 -0
  317. workgraph/gates.py +150 -0
  318. workgraph/operations.py +288 -0
  319. workgraph/output.py +89 -0
  320. workgraph/py.typed +0 -0
  321. workgraph/shutdown.py +94 -0
  322. workgraph/workloads.py +145 -0
@@ -0,0 +1,38 @@
1
+ """
2
+ Property-based tests for Helm charts.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from hypothesis_helm.charts.generate import coalesce, generate_tests
9
+ from hypothesis_helm.charts.model import Chart
10
+ from hypothesis_helm.charts.runner import check_chart
11
+
12
+ __all__ = ["Chart", "check_chart", "coalesce", "generate_tests"]
13
+
14
+
15
+ def __getattr__(name: str) -> object:
16
+ """
17
+ Load the public API without constructing strategies during pytest plugin startup.
18
+
19
+ Args:
20
+ name (str): Public package attribute requested by the caller.
21
+
22
+ Returns:
23
+ object: The exported chart type or framework function.
24
+ """
25
+ if name not in __all__:
26
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
27
+ from hypothesis_helm.charts.generate import coalesce, generate_tests
28
+ from hypothesis_helm.charts.model import Chart
29
+ from hypothesis_helm.charts.runner import check_chart
30
+
31
+ exports: dict[str, object] = {
32
+ "Chart": Chart,
33
+ "check_chart": check_chart,
34
+ "coalesce": coalesce,
35
+ "generate_tests": generate_tests,
36
+ }
37
+ globals().update(exports)
38
+ return exports[name]
@@ -0,0 +1,3 @@
1
+ """
2
+ Measure configuration mutations without changing compiler pruning guarantees.
3
+ """
@@ -0,0 +1,117 @@
1
+ """
2
+ Measure the downstream effects of explicit values mutations without changing a chart.
3
+ """
4
+
5
+ import argparse
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+
10
+ from hypothesis_helm.analysis.report import write_report
11
+ from hypothesis_helm.analysis.sensitivity import Mutation, analyze
12
+ from hypothesis_helm.charts.model import Chart
13
+ from hypothesis_helm.charts.rendering import render
14
+ from hypothesis_helm.reporting.budget import parse_time_limit
15
+ from hypothesis_helm.schemas.contracts import mapping, sequence
16
+
17
+
18
+ def load_mutations(path: Path) -> list[Mutation]:
19
+ """
20
+ Read explicit names, typed paths and replacement values from JSON.
21
+
22
+ Args:
23
+ path (Path): JSON array of mutation descriptors.
24
+
25
+ Returns:
26
+ list[Mutation]: Validated operation descriptors in input order.
27
+ """
28
+ mutations = []
29
+ for entry in sequence(json.loads(path.read_text())):
30
+ record = mapping(entry)
31
+ name, parts = record["name"], sequence(record["path"])
32
+ if not isinstance(name, str) or not name or not parts:
33
+ raise ValueError("each mutation requires a nonempty string name and path")
34
+ path_parts: list[str | int] = []
35
+ for part in parts:
36
+ if isinstance(part, str) or type(part) is int:
37
+ path_parts.append(part)
38
+ else:
39
+ raise ValueError("path components must be string keys or integer indices")
40
+ mutations.append(Mutation(name, tuple(path_parts), record["value"]))
41
+ return mutations
42
+
43
+
44
+ def main(argv: list[str] | None = None) -> int:
45
+ """
46
+ Run a bounded local-chart sensitivity analysis and save its evidence.
47
+
48
+ Args:
49
+ argv (list[str] | None): Explicit arguments or process command line.
50
+
51
+ Returns:
52
+ int: Zero for a complete analysis, one for a budget-limited result.
53
+ """
54
+ parser = argparse.ArgumentParser(description=__doc__)
55
+ parser.add_argument("chart", type=Path, help="local chart containing values.yaml and values.schema.json")
56
+ parser.add_argument("--mutations", required=True, type=Path, help="JSON array of name/path/value replacements")
57
+ parser.add_argument("--output", type=Path, default=Path(f"studies/sensitivity/runs/{time.time_ns()}"))
58
+ parser.add_argument("--max-pairs", type=int, default=100)
59
+ parser.add_argument("--max-mutations", type=int, default=100)
60
+ parser.add_argument("--time-limit", type=parse_time_limit, default=180)
61
+ parser.add_argument("--helm", default="helm")
62
+ parser.add_argument("--release", default="hypothesis")
63
+ parser.add_argument("--namespace", default="default")
64
+ parser.add_argument("--kube-version")
65
+ parser.add_argument("--plot", action="store_true", help="requires the benchmarking extra (matplotlib)")
66
+ args = parser.parse_args(argv)
67
+ if args.output.exists():
68
+ parser.error("choose a fresh output directory")
69
+ if args.max_pairs < 0 or args.max_mutations < 1:
70
+ parser.error("max-pairs must be nonnegative and max-mutations must be positive")
71
+ chart = Chart.load(args.chart)
72
+ mutations = load_mutations(args.mutations)
73
+ deadline = time.monotonic() + args.time_limit
74
+
75
+ def invoke(values: dict[str, object]) -> object:
76
+ """
77
+ Render with a fixed invocation context and the remaining execution budget.
78
+
79
+ Args:
80
+ values (dict[str, object]): Schema-valid values configuration.
81
+
82
+ Returns:
83
+ object: Parsed, validated manifest bundle.
84
+ """
85
+ remaining = deadline - time.monotonic()
86
+ if remaining <= 0:
87
+ raise TimeoutError("analysis render deadline reached")
88
+ return render(
89
+ chart,
90
+ values,
91
+ helm=args.helm,
92
+ release=args.release,
93
+ namespace=args.namespace,
94
+ kube_version=args.kube_version,
95
+ timeout=min(30, remaining),
96
+ stream=False,
97
+ )
98
+
99
+ document = analyze(
100
+ chart.defaults,
101
+ chart.schema,
102
+ mutations,
103
+ invoke,
104
+ max_pairs=args.max_pairs,
105
+ max_mutations=args.max_mutations,
106
+ time_limit=args.time_limit,
107
+ )
108
+ document["context"] = {
109
+ "chart": str(chart.path),
110
+ "helm": args.helm,
111
+ "release": args.release,
112
+ "namespace": args.namespace,
113
+ "kube_version": args.kube_version,
114
+ }
115
+ write_report(args.output, document, plots=args.plot)
116
+ print(f"Sensitivity analysis: {document['status']}; {document['renders']} renders; report: {args.output / 'README.md'}")
117
+ return 0 if document["status"] == "complete" else 1
@@ -0,0 +1,172 @@
1
+ """
2
+ Publish concise sensitivity evidence and optional diagnostic plots.
3
+ """
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from hypothesis_helm.reporting.contents import with_contents
9
+ from hypothesis_helm.schemas.contracts import mapping, sequence
10
+
11
+
12
+ def write_report(output: Path, document: dict[str, object], *, plots: bool = False) -> None:
13
+ """
14
+ Save measurements, a human-readable summary and optional matplotlib plots.
15
+
16
+ Args:
17
+ output (Path): Fresh report directory.
18
+ document (dict[str, object]): Completed or partial sensitivity evidence.
19
+ plots (bool): Generate a three-panel PNG and SVG.
20
+
21
+ Returns:
22
+ None: Report artifacts are written.
23
+ """
24
+ output.mkdir(parents=True, exist_ok=False)
25
+ (output / "results.json").write_text(json.dumps(document, indent=2, allow_nan=False) + "\n")
26
+ rows = [mapping(row) for row in sequence(document["mutations"])]
27
+ pairs = [mapping(row) for row in sequence(document["interactions"])]
28
+ identifiers = {str(row["name"]): index + 1 for index, row in enumerate(rows)}
29
+ lines = [
30
+ "# Mutation sensitivity",
31
+ "",
32
+ f"Status: **{document['status']}**. Helm render attempts: **{document['renders']}**.",
33
+ "",
34
+ "Distance counts added and removed JSON path/value indicators. A changed value counts twice.",
35
+ "Document and array order matter. These measurements do not prove equivalence or authorize pruning.",
36
+ "Distances assume deterministic rendering with fixed chart dependencies, release, namespace and Kubernetes version.",
37
+ "",
38
+ ]
39
+ if plots:
40
+ plot(output, document)
41
+ lines.extend(["![Sensitivity, interaction and sequence measurements](sensitivity.png)", ""])
42
+ lines += [
43
+ f"Measured {len(rows)} single mutations and {len(pairs)} pairs. The plots include every comparable measurement.",
44
+ "Tables show up to 20 of the largest effects. Mutation IDs follow the order in mutations.json.",
45
+ "",
46
+ "| ID | Mutation | Values path | Replacement | Output distance | Status |",
47
+ "| ---: | --- | --- | --- | ---: | --- |",
48
+ ]
49
+
50
+ def literal(value: object) -> str:
51
+ """
52
+ Escape untrusted labels and values for Markdown table cells.
53
+
54
+ Args:
55
+ value (object): JSON value to display.
56
+
57
+ Returns:
58
+ str: HTML-escaped JSON with table separators encoded.
59
+ """
60
+ import html
61
+
62
+ return html.escape(json.dumps(value, ensure_ascii=True)).replace("|", "&#124;")
63
+
64
+ for row in sorted(rows, key=lambda row: -int(str(row.get("distance", -1))))[:20]:
65
+ lines.append(
66
+ f"| {identifiers[str(row['name'])]} | {literal(row['name'])} | {literal(row['path'])} | {literal(row['value'])} | "
67
+ f"{row.get('distance', 'N/A')} | {row['status']} |"
68
+ )
69
+ lines += [
70
+ "",
71
+ "## Parameter interactions",
72
+ "",
73
+ "A nonzero mixed difference means the selected output features respond non-additively.",
74
+ "Pairs with order-dependent inputs are excluded from this measure. Render failures have no assigned distance.",
75
+ "",
76
+ "| Mutations | Mixed difference | Status |",
77
+ "| --- | ---: | --- |",
78
+ ]
79
+ for row in sorted(pairs, key=lambda row: -int(str(row.get("mixed_difference_l1", -1))))[:20]:
80
+ lines.append(f"| {literal(row['mutations'])} | {row.get('mixed_difference_l1', 'N/A')} | {row['status']} |")
81
+ lines += [
82
+ "",
83
+ "The ordered sequence in results.json records both cumulative path length and displacement from the baseline.",
84
+ "They differ when later mutations reverse earlier changes. The sequence stops at its first invalid or failed step.",
85
+ "",
86
+ "[Full measurements and render errors](results.json)",
87
+ ]
88
+ (output / "README.md").write_text(with_contents("\n".join(lines) + "\n"))
89
+
90
+
91
+ def plot(output: Path, document: dict[str, object]) -> None:
92
+ """
93
+ Plot mutation distances, pair interactions and ordered path accumulation.
94
+
95
+ Args:
96
+ output (Path): Destination for plot files.
97
+ document (dict[str, object]): Recorded sensitivity observations.
98
+
99
+ Returns:
100
+ None: PNG and SVG plots are saved.
101
+ """
102
+ import numpy as np
103
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
104
+ from matplotlib.figure import Figure
105
+
106
+ figure = Figure(figsize=(20, 7), layout="constrained")
107
+ FigureCanvasAgg(figure)
108
+ axes = figure.subplots(1, 3)
109
+ mutations = [mapping(row) for row in sequence(document["mutations"])]
110
+ names = [str(row["name"]) for row in mutations]
111
+ singles = [(index + 1, int(str(row["distance"]))) for index, row in enumerate(mutations) if "distance" in row]
112
+ if singles:
113
+ axes[0].scatter(*zip(*singles, strict=True), s=22, alpha=0.8)
114
+ else:
115
+ axes[0].text(0.5, 0.5, "No comparable single mutations", transform=axes[0].transAxes, ha="center")
116
+ axes[0].set(
117
+ title=f"Which single changes affect output most?\n{len(singles)} measured mutations; IDs follow the input file",
118
+ xlabel="Mutation ID",
119
+ ylabel="Changed leaf indicators",
120
+ )
121
+ pairs = [mapping(row) for row in sequence(document["interactions"]) if "mixed_difference_l1" in mapping(row)]
122
+ positions = {name: index for index, name in enumerate(names)}
123
+ matrix = np.full((len(names), len(names)), np.nan)
124
+ for row in pairs:
125
+ left, right = (positions[str(name)] for name in sequence(row["mutations"]))
126
+ matrix[left, right] = matrix[right, left] = int(str(row["mixed_difference_l1"]))
127
+ if pairs:
128
+ from matplotlib import colormaps
129
+
130
+ colors = colormaps["viridis"].with_extremes(bad="#dddddd")
131
+ heatmap = axes[1].imshow(
132
+ np.ma.masked_invalid(matrix),
133
+ origin="lower",
134
+ interpolation="nearest",
135
+ cmap=colors,
136
+ vmin=0,
137
+ vmax=max(1, float(np.nanmax(matrix))),
138
+ extent=(0.5, len(names) + 0.5, 0.5, len(names) + 0.5),
139
+ )
140
+ figure.colorbar(heatmap, ax=axes[1], label="Interaction magnitude", shrink=0.75)
141
+ else:
142
+ axes[1].text(0.5, 0.5, "No comparable pairs", transform=axes[1].transAxes, ha="center")
143
+ axes[1].set(
144
+ title=f"Which pairs interact?\n{len(pairs)} measured pairs; gray = unmeasured or inapplicable",
145
+ xlabel="Mutation ID",
146
+ ylabel="Mutation ID",
147
+ )
148
+ steps = [mapping(row) for row in sequence(document["sequence"]) if "cumulative_path_length" in mapping(row)]
149
+ indices = list(range(len(steps) + 1))
150
+ if steps:
151
+ axes[2].plot(indices, [0, *(int(str(row["cumulative_path_length"])) for row in steps)], marker=".", label="Cumulative path length")
152
+ axes[2].plot(
153
+ indices, [0, *(int(str(row["endpoint_displacement"])) for row in steps)], marker=".", label="Displacement from baseline"
154
+ )
155
+ axes[2].legend()
156
+ else:
157
+ axes[2].text(0.5, 0.5, "No comparable sequence", transform=axes[2].transAxes, ha="center")
158
+ axes[2].set(
159
+ title=f"Do later changes undo earlier ones?\n{len(steps)} measured steps in explicit input order",
160
+ xlabel="Completed mutation steps",
161
+ ylabel="Leaf indicators",
162
+ )
163
+ for index, axis in enumerate(axes):
164
+ axis.title.set_fontsize(10)
165
+ axis.tick_params(labelsize=9)
166
+ if index != 1:
167
+ axis.grid(alpha=0.2)
168
+ heading = figure.suptitle("Which values changes have the largest effects, and which interact?")
169
+ heading.set_gid("plot-question")
170
+ for suffix in ("png", "svg"):
171
+ figure.savefig(output / f"sensitivity.{suffix}", dpi=240)
172
+ figure.clear()
@@ -0,0 +1,268 @@
1
+ """
2
+ Measure output sensitivity and interactions of explicit chart-value mutations.
3
+ """
4
+
5
+ import copy
6
+ import itertools
7
+ import json
8
+ import time
9
+ from collections import Counter
10
+ from collections.abc import Callable, Sequence
11
+ from typing import cast
12
+
13
+ from attrs import define
14
+ from jsonschema import validators
15
+
16
+ from hypothesis_helm.schemas.contracts import Json, configuration_key
17
+
18
+
19
+ @define(frozen=True)
20
+ class Mutation:
21
+ """
22
+ Replace one value at an explicit object or array path.
23
+
24
+ Attributes:
25
+ name (str): Unique mutation label.
26
+ path (tuple[str | int, ...]): Object keys and array indices.
27
+ value (object): JSON-compatible replacement value.
28
+ """
29
+
30
+ name: str
31
+ path: tuple[str | int, ...]
32
+ value: object
33
+
34
+ def apply(self, values: dict[str, object]) -> dict[str, object]:
35
+ """
36
+ Apply a replacement without modifying the original configuration.
37
+
38
+ Args:
39
+ values (dict[str, object]): Source configuration.
40
+
41
+ Returns:
42
+ dict[str, object]: Independently owned mutated configuration.
43
+ """
44
+ if not self.name or not self.path:
45
+ raise ValueError("mutations need a name and a nonempty path")
46
+ result = copy.deepcopy(values)
47
+ node: object = result
48
+ for part in self.path[:-1]:
49
+ if isinstance(node, dict) and isinstance(part, str):
50
+ node = node[part]
51
+ elif isinstance(node, list) and type(part) is int and 0 <= part < len(node):
52
+ node = node[part]
53
+ else:
54
+ raise ValueError("mutation path does not address an existing container")
55
+ part = self.path[-1]
56
+ if isinstance(node, dict) and isinstance(part, str):
57
+ node[part] = copy.deepcopy(self.value)
58
+ elif isinstance(node, list) and type(part) is int and 0 <= part < len(node):
59
+ node[part] = copy.deepcopy(self.value)
60
+ else:
61
+ raise ValueError("mutation path does not address a replaceable value")
62
+ configuration_key(result)
63
+ return result
64
+
65
+
66
+ def features(value: object, path: tuple[str | int, ...] = ()) -> Counter[str]:
67
+ """
68
+ Encode manifest leaves as exact path-and-value indicators.
69
+
70
+ Args:
71
+ value (object): JSON manifest bundle or subtree.
72
+ path (tuple[str | int, ...]): Current structural position.
73
+
74
+ Returns:
75
+ Counter[str]: One-hot leaf features, including empty containers.
76
+ """
77
+ result: Counter[str] = Counter()
78
+ if isinstance(value, dict) and value:
79
+ for key, item in value.items():
80
+ result.update(features(item, (*path, str(key))))
81
+ elif isinstance(value, list) and value:
82
+ for index, item in enumerate(value):
83
+ result.update(features(item, (*path, index)))
84
+ else:
85
+ result[json.dumps([path, value], sort_keys=True, allow_nan=False)] += 1
86
+ return result
87
+
88
+
89
+ def distance(left: object, right: object) -> int:
90
+ """
91
+ Compute L1 displacement between leaf-indicator representations.
92
+
93
+ Args:
94
+ left (object): First parsed manifest bundle.
95
+ right (object): Second parsed manifest bundle.
96
+
97
+ Returns:
98
+ int: Added plus removed path/value indicators; a replacement counts twice.
99
+ """
100
+ a, b = features(left), features(right)
101
+ return sum(abs(a[key] - b[key]) for key in a.keys() | b.keys())
102
+
103
+
104
+ def analyze(
105
+ baseline: dict[str, object],
106
+ schema: dict[str, object],
107
+ mutations: Sequence[Mutation],
108
+ render: Callable[[dict[str, object]], object],
109
+ *,
110
+ max_pairs: int = 100,
111
+ max_mutations: int = 100,
112
+ time_limit: float = 180,
113
+ ) -> dict[str, object]:
114
+ """
115
+ Measure single mutations, pairwise interactions and a cumulative mutation sequence.
116
+
117
+ Args:
118
+ baseline (dict[str, object]): Starting values configuration.
119
+ schema (dict[str, object]): JSON Schema restricting all measured configurations.
120
+ mutations (Sequence[Mutation]): Explicit replacement operations in sequence order.
121
+ render (Callable[[dict[str, object]], object]): Renderer with fixed chart and invocation context.
122
+ max_pairs (int): Maximum unordered pairs of selected mutations to examine.
123
+ max_mutations (int): Maximum supplied mutations to examine.
124
+ time_limit (float): Admission deadline; callers must also bound individual render invocations.
125
+
126
+ Returns:
127
+ dict[str, object]: Sensitivity rankings, interactions, sequence distances and explicit failures.
128
+ """
129
+ if max_pairs < 0 or max_mutations < 1 or not 0 < time_limit < float("inf"):
130
+ raise ValueError("analysis limits must be nonnegative pairs, positive mutations and finite positive time")
131
+ if not mutations or len({mutation.name for mutation in mutations}) != len(mutations):
132
+ raise ValueError("provide at least one mutation with unique names")
133
+ validator = validators.validator_for(schema)(schema)
134
+ validator.validate(cast(Json, baseline))
135
+ deadline = time.monotonic() + time_limit
136
+ selected = list(mutations[:max_mutations])
137
+ observations: dict[str, dict[str, object]] = {}
138
+ singles: list[dict[str, object]] = []
139
+ interactions: list[dict[str, object]] = []
140
+ sequence_rows: list[dict[str, object]] = []
141
+ input_values: dict[str, dict[str, object]] = {}
142
+ renders = 0
143
+
144
+ def observe(values: dict[str, object]) -> dict[str, object]:
145
+ """
146
+ Render a schema-valid configuration once within this analysis.
147
+
148
+ Args:
149
+ values (dict[str, object]): Candidate chart input.
150
+
151
+ Returns:
152
+ dict[str, object]: Output evidence or a schema/render failure.
153
+ """
154
+ nonlocal renders
155
+ if time.monotonic() >= deadline:
156
+ raise TimeoutError("analysis admission deadline reached")
157
+ key = configuration_key(values)
158
+ if key in observations:
159
+ return observations[key]
160
+ from jsonschema.exceptions import ValidationError
161
+
162
+ try:
163
+ validator.validate(cast(Json, values))
164
+ except ValidationError as error:
165
+ return {"status": "schema-rejected", "error": error.message}
166
+ renders += 1
167
+ try:
168
+ output = render(copy.deepcopy(values))
169
+ json.dumps(output, sort_keys=True, allow_nan=False)
170
+ observation: dict[str, object] = {"status": "rendered", "output": output}
171
+ except Exception as error:
172
+ observation = {"status": "render-error", "error": str(error), "error_type": type(error).__name__}
173
+ observations[key] = observation
174
+ return observation
175
+
176
+ origin: dict[str, object] = {"status": "unobserved"}
177
+ status = "complete"
178
+ try:
179
+ origin = observe(baseline)
180
+ for mutation in selected:
181
+ row: dict[str, object] = {"name": mutation.name, "path": list(mutation.path), "value": mutation.value}
182
+ try:
183
+ values = mutation.apply(baseline)
184
+ except (ValueError, KeyError, IndexError, TypeError) as error:
185
+ singles.append({**row, "status": "invalid-mutation", "error": str(error)})
186
+ continue
187
+ observation = observe(values)
188
+ row.update(observation)
189
+ if observation["status"] == "rendered" and origin["status"] == "rendered":
190
+ row["distance"] = distance(origin["output"], observation["output"])
191
+ input_values[mutation.name] = values
192
+ singles.append(row)
193
+ for first, second in itertools.islice(itertools.combinations(selected, 2), max_pairs):
194
+ row = {"mutations": [first.name, second.name]}
195
+ if first.name not in input_values or second.name not in input_values:
196
+ interactions.append({**row, "status": "unavailable-single"})
197
+ continue
198
+ a, b = input_values[first.name], input_values[second.name]
199
+ try:
200
+ ab, ba = second.apply(a), first.apply(b)
201
+ except (ValueError, KeyError, IndexError, TypeError) as error:
202
+ interactions.append({**row, "status": "invalid-mutation", "error": str(error)})
203
+ continue
204
+ if configuration_key(ab) != configuration_key(ba):
205
+ interactions.append({**row, "status": "order-dependent"})
206
+ continue
207
+ joint = observe(ab)
208
+ row.update(joint)
209
+ observation_a, observation_b = observe(a), observe(b)
210
+ comparable = all(item["status"] == "rendered" for item in (origin, observation_a, observation_b, joint))
211
+ if comparable:
212
+ fa, fb, fab, f0 = (
213
+ features(observation_a["output"]),
214
+ features(observation_b["output"]),
215
+ features(joint["output"]),
216
+ features(origin["output"]),
217
+ )
218
+ keys = fa.keys() | fb.keys() | fab.keys() | f0.keys()
219
+ row["mixed_difference_l1"] = sum(abs(fab[key] - fa[key] - fb[key] + f0[key]) for key in keys)
220
+ row["joint_distance"] = distance(origin["output"], joint["output"])
221
+ else:
222
+ row["measurement_status"] = "unavailable-reference"
223
+ interactions.append(row)
224
+ current, previous = copy.deepcopy(baseline), origin
225
+ cumulative = 0
226
+ for mutation in selected:
227
+ try:
228
+ next_values = mutation.apply(current)
229
+ except (ValueError, KeyError, IndexError, TypeError) as error:
230
+ sequence_rows.append({"name": mutation.name, "status": "invalid-mutation", "error": str(error)})
231
+ break
232
+ next_observation = observe(next_values)
233
+ row = {"name": mutation.name, "status": next_observation["status"]}
234
+ if next_observation["status"] != "rendered" or previous["status"] != "rendered":
235
+ if previous["status"] != "rendered":
236
+ row["status"] = "preceding-output-unavailable"
237
+ row["error"] = next_observation.get("error", "preceding output unavailable")
238
+ sequence_rows.append(row)
239
+ break
240
+ step_distance = distance(previous["output"], next_observation["output"])
241
+ cumulative += step_distance
242
+ row.update(
243
+ step_distance=step_distance,
244
+ cumulative_path_length=cumulative,
245
+ endpoint_displacement=distance(origin["output"], next_observation["output"]),
246
+ )
247
+ sequence_rows.append(row)
248
+ current, previous = next_values, next_observation
249
+ except TimeoutError:
250
+ status = "time-limit"
251
+ if time.monotonic() >= deadline:
252
+ status = "time-limit"
253
+ if status == "complete" and (len(selected) < len(mutations) or len(selected) * (len(selected) - 1) // 2 > max_pairs):
254
+ status = "selection-limit"
255
+ return {
256
+ "version": 1,
257
+ "status": status,
258
+ "metric": "L1 of JSON path/value leaf indicators; arrays and documents remain ordered",
259
+ "baseline": origin,
260
+ "mutations": singles,
261
+ "interactions": interactions,
262
+ "sequence": sequence_rows,
263
+ "selected_mutations": len(selected),
264
+ "provided_mutations": len(mutations),
265
+ "renders": renders,
266
+ "pruning_authorized": False,
267
+ "assumption": "The renderer is deterministic under a fixed chart, dependency and invocation context.",
268
+ }
@@ -0,0 +1,3 @@
1
+ """
2
+ Reproducible renderer, pruning and worker-scaling benchmarks.
3
+ """
@@ -0,0 +1,3 @@
1
+ """
2
+ Analyze output populations, calibration and filtering selections.
3
+ """