From 2dc7a4f1a53cf801156f09fd84e6a6e4d1c27baf Mon Sep 17 00:00:00 2001 From: SamGu-NRX Date: Sat, 26 Sep 2026 07:02:41 -0500 Subject: [PATCH 1/4] experiments(panel-label): pre-register questions; crawler and Vision reader Co-Authored-By: Claude Opus 5.5 (1M context) --- experiments/panel-label/.gitignore | 2 + experiments/panel-label/README.md | 11 + .../panel-label/meterocr/Package.swift | 10 + .../meterocr/Sources/meterocr/main.swift | 172 +++++++++ experiments/panel-label/pyproject.toml | 29 ++ .../panel-label/src/panel_eval/__init__.py | 0 .../panel-label/src/panel_eval/paths.py | 11 + .../panel-label/src/panel_eval/sources.py | 240 +++++++++++++ experiments/panel-label/uv.lock | 333 ++++++++++++++++++ 9 files changed, 808 insertions(+) create mode 100644 experiments/panel-label/.gitignore create mode 100644 experiments/panel-label/README.md create mode 100644 experiments/panel-label/meterocr/Package.swift create mode 100644 experiments/panel-label/meterocr/Sources/meterocr/main.swift create mode 100644 experiments/panel-label/pyproject.toml create mode 100644 experiments/panel-label/src/panel_eval/__init__.py create mode 100644 experiments/panel-label/src/panel_eval/paths.py create mode 100644 experiments/panel-label/src/panel_eval/sources.py create mode 100644 experiments/panel-label/uv.lock diff --git a/experiments/panel-label/.gitignore b/experiments/panel-label/.gitignore new file mode 100644 index 000000000..2d9f16e2d --- /dev/null +++ b/experiments/panel-label/.gitignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ diff --git a/experiments/panel-label/README.md b/experiments/panel-label/README.md new file mode 100644 index 000000000..6cd2628af --- /dev/null +++ b/experiments/panel-label/README.md @@ -0,0 +1,11 @@ +# Panel label readability + +Installers check the main electrical panel's manufacturer, model and main breaker amperage. Some panel lines are known hazards: Federal Pacific Stab-Lok and Zinsco are both subjects of public safety notices. The main breaker's amperage matters for any battery install. If the phone can read a close-up of the panel label, a homeowner can answer with one photo instead of a form. This experiment measures that on real, openly licensed panel photos, reusing the meter close-up tooling. + +## Questions and pass criteria, set before the run + +1. How often does Vision's `accurate` mode read the manufacturer, the model or series, and the main breaker's amperage? Each field is scored on the photos where both AI readers agree it is legible, under the strict rule (both readers transcribed the same text and were sure). One photo can replace the form if the manufacturer and the main amperage are each read on at least 90% of those photos; below 70%, keep the form. +2. Can a simple rule map the recognized text to a manufacturer from a small public list, including the known-hazard lines? Precision (the named manufacturer is right, among photos where the rule names one) and recall (over photos with an agreed manufacturer) are reported on held-out photos. The rule may name a manufacturer without asking the homeowner if its held-out precision is at least 95%. To limit fitting, the rules are written using only odd-numbered photos and committed before the even-numbered ones are scored. +3. Do the meter close-up's blur and too-small checks transfer to panel labels? They transfer if, on the same controlled degradations of the panel photos, the panel's own 95% break-point threshold for each check is no stricter than the meter's (whole-photo sharpness 6.63; top-candidate line height 33.9 px). Otherwise the panel needs its own thresholds, which are reported. + +If fewer than about 40 usable openly licensed photos exist, the experiment stops there and reports what was searched. diff --git a/experiments/panel-label/meterocr/Package.swift b/experiments/panel-label/meterocr/Package.swift new file mode 100644 index 000000000..c4445b63c --- /dev/null +++ b/experiments/panel-label/meterocr/Package.swift @@ -0,0 +1,10 @@ +// swift-tools-version: 6.0 +import PackageDescription + +let package = Package( + name: "meterocr", + platforms: [.macOS(.v14)], + targets: [ + .executableTarget(name: "meterocr") + ] +) diff --git a/experiments/panel-label/meterocr/Sources/meterocr/main.swift b/experiments/panel-label/meterocr/Sources/meterocr/main.swift new file mode 100644 index 000000000..06c56c635 --- /dev/null +++ b/experiments/panel-label/meterocr/Sources/meterocr/main.swift @@ -0,0 +1,172 @@ +// Reads text from images with Apple Vision (VNRecognizeTextRequest), the engine the iPhone app +// would call. One JSON request per stdin line, one JSON result per stdout line, so a Python +// driver can stream thousands of degraded images through a single process. +// +// Request: {"path": "...", "level": "accurate"|"fast", "language_correction": false, +// "crop": [x, y, w, h], // optional, normalized, top-left origin +// "barcodes": false} // true also runs VNDetectBarcodesRequest (revision 4) +// Result: {"path", "width", "height", "elapsed_ms", "lines": [{"text", "confidence", +// "box": [x, y, w, h], "candidates": [...]}], // box normalized, top-left origin +// "barcodes": [{"payload", "symbology", "confidence", "box"}]} // when requested +// +// Usage: meterocr < requests.jsonl or meterocr [--fast] [--lc] image... + +import CoreGraphics +import Foundation +import ImageIO +import Vision + +struct Request: Decodable { + let path: String + var level: String? = "accurate" + var language_correction: Bool? = false + var crop: [Double]? = nil + var barcodes: Bool? = false +} + +struct Line: Encodable { + let text: String + let confidence: Float + let box: [Double] + let candidates: [String] +} + +struct Barcode: Encodable { + let payload: String? + let symbology: String + let confidence: Float + let box: [Double] +} + +struct Result: Encodable { + let path: String + let level: String + let language_correction: Bool + let crop: [Double]? + let width: Int + let height: Int + let elapsed_ms: Double + let lines: [Line] + let barcodes: [Barcode]? + let error: String? +} + +enum ReadError: Error, CustomStringConvertible { + case unreadableImage(String) + case badCrop([Double]) + + var description: String { + switch self { + case .unreadableImage(let path): "cannot decode image at \(path)" + case .badCrop(let crop): "crop must be 4 normalized values inside [0, 1], got \(crop)" + } + } +} + +func loadImage(_ path: String) throws -> CGImage { + let url = URL(fileURLWithPath: path) as CFURL + guard let source = CGImageSourceCreateWithURL(url, nil), + let image = CGImageSourceCreateImageAtIndex(source, 0, nil) + else { throw ReadError.unreadableImage(path) } + return image +} + +func read(_ request: Request) -> Result { + let level = request.level ?? "accurate" + let correction = request.language_correction ?? false + let started = Date() + var width = 0 + var height = 0 + do { + var image = try loadImage(request.path) + if let crop = request.crop { + guard crop.count == 4, crop.allSatisfy({ (0...1).contains($0) }), + crop[0] + crop[2] <= 1.0001, crop[1] + crop[3] <= 1.0001 + else { throw ReadError.badCrop(crop) } + let rect = CGRect( + x: crop[0] * Double(image.width), y: crop[1] * Double(image.height), + width: crop[2] * Double(image.width), height: crop[3] * Double(image.height) + ).integral + guard let cropped = image.cropping(to: rect) else { throw ReadError.badCrop(crop) } + image = cropped + } + width = image.width + height = image.height + + let vision = VNRecognizeTextRequest() + vision.revision = VNRecognizeTextRequestRevision3 + vision.recognitionLevel = level == "fast" ? .fast : .accurate + vision.usesLanguageCorrection = correction + vision.recognitionLanguages = ["en-US"] + // Default is already 0.0 (full resolution, per the SDK header); set it so a future + // default change cannot silently drop small labels. + vision.minimumTextHeight = 0 + let barcodeRequest = VNDetectBarcodesRequest() + barcodeRequest.revision = VNDetectBarcodesRequestRevision4 + let wantsBarcodes = request.barcodes ?? false + try VNImageRequestHandler(cgImage: image, orientation: .up) + .perform(wantsBarcodes ? [vision, barcodeRequest] : [vision]) + let barcodes = wantsBarcodes + ? (barcodeRequest.results ?? []).map { observation in + let b = observation.boundingBox + return Barcode( + payload: observation.payloadStringValue, + symbology: observation.symbology.rawValue, + confidence: observation.confidence, + box: [b.minX, 1 - b.maxY, b.width, b.height]) + } + : nil + + let lines = (vision.results ?? []).compactMap { observation -> Line? in + let top = observation.topCandidates(3) + guard let best = top.first else { return nil } + let b = observation.boundingBox // normalized, bottom-left origin + return Line( + text: best.string, + confidence: best.confidence, + box: [b.minX, 1 - b.maxY, b.width, b.height], + candidates: top.map(\.string) + ) + } + return Result( + path: request.path, level: level, language_correction: correction, crop: request.crop, + width: width, height: height, elapsed_ms: Date().timeIntervalSince(started) * 1000, + lines: lines, barcodes: barcodes, error: nil) + } catch { + return Result( + path: request.path, level: level, language_correction: correction, crop: request.crop, + width: width, height: height, elapsed_ms: Date().timeIntervalSince(started) * 1000, + lines: [], barcodes: nil, error: String(describing: error)) + } +} + +let encoder = JSONEncoder() +encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + +func emit(_ result: Result) { + let data = try! encoder.encode(result) + FileHandle.standardOutput.write(data) + FileHandle.standardOutput.write(Data("\n".utf8)) +} + +var arguments = Array(CommandLine.arguments.dropFirst()) +if arguments.isEmpty { + let decoder = JSONDecoder() + while let line = readLine() { + guard !line.isEmpty else { continue } + do { + let request = try decoder.decode(Request.self, from: Data(line.utf8)) + autoreleasepool { emit(read(request)) } + } catch { + FileHandle.standardError.write(Data("bad request line: \(line)\n".utf8)) + exit(2) + } + } +} else { + let fast = arguments.contains("--fast") + let correction = arguments.contains("--lc") + arguments.removeAll { $0.hasPrefix("--") } + for path in arguments { + emit(read(Request(path: path, level: fast ? "fast" : "accurate", language_correction: correction))) + } +} diff --git a/experiments/panel-label/pyproject.toml b/experiments/panel-label/pyproject.toml new file mode 100644 index 000000000..23cc3c063 --- /dev/null +++ b/experiments/panel-label/pyproject.toml @@ -0,0 +1,29 @@ +[project] +name = "panel-label" +version = "0.1.0" +description = "Whether on-device text recognition reads residential panel labels: manufacturer, model and main breaker amperage" +requires-python = ">=3.12" +dependencies = [ + "numpy>=2.1", + "pillow>=11.0", + "scipy>=1.14", +] + +[dependency-groups] +dev = ["pytest>=8.3", "ruff>=0.8"] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/panel_eval"] + +[tool.uv] +required-version = ">=0.11.15,<0.12" + +[tool.ruff] +line-length = 100 + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/experiments/panel-label/src/panel_eval/__init__.py b/experiments/panel-label/src/panel_eval/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/experiments/panel-label/src/panel_eval/paths.py b/experiments/panel-label/src/panel_eval/paths.py new file mode 100644 index 000000000..d65d67445 --- /dev/null +++ b/experiments/panel-label/src/panel_eval/paths.py @@ -0,0 +1,11 @@ +import os +from pathlib import Path + +EXPERIMENT_DIR = Path(__file__).resolve().parents[2] +MANIFEST = EXPERIMENT_DIR / "manifest.csv" +RESULTS_DIR = EXPERIMENT_DIR / "results" +# Photos and raw recognizer output stay outside the repository. +DATA_DIR = Path(os.environ.get("PANEL_DATA", Path.home() / "house-scanning-data" / "panel")) +# The label review page and the HMAC key live here, outside the repository too. +REVIEW_DIR = DATA_DIR.parent / "panel-label" +KEY_PATH = REVIEW_DIR / "hmac.key" diff --git a/experiments/panel-label/src/panel_eval/sources.py b/experiments/panel-label/src/panel_eval/sources.py new file mode 100644 index 000000000..4eff75a4e --- /dev/null +++ b/experiments/panel-label/src/panel_eval/sources.py @@ -0,0 +1,240 @@ +"""Find and download openly licensed photos of residential electrical panels. + +`find` runs a fixed list of searches on Wikimedia Commons and on Openverse (an index of +openly licensed images from Flickr, Commons and other sources) and writes every result under +CC0, public domain, CC BY or CC BY-SA to candidates.csv in the data directory, for screening +by eye. `fetch` downloads the photos listed in the committed manifest.csv. +""" + +import argparse +import csv +import io +import json +import re +import time +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path + +from PIL import Image, ImageOps + +from panel_eval.paths import DATA_DIR, MANIFEST + +USER_AGENT = "house-scanning-panel-eval/0.1 (https://github.com/SamGu-NRX/house-scanning)" +COMMONS_API = "https://commons.wikimedia.org/w/api.php" +OPENVERSE_API = "https://api.openverse.org/v1/images/" +SEARCHES = [ + "breaker panel", + "electrical panel", + "circuit breaker panel", + "circuit breaker box", + "breaker box", + "load center", + "service panel", + "electrical service panel", + "main breaker", + "panelboard", + "Square D panel", + "Federal Pacific", + "Stab-Lok", + "Zinsco", + "Cutler-Hammer panel", + "Siemens breaker panel", + "General Electric breaker panel", + "Murray breaker panel", + "Challenger breaker panel", + "Westinghouse breaker panel", +] +# Stored photos are capped at this width, a standard Commons thumbnail step. +MAX_WIDTH = 3840 +REUSABLE_COMMONS = re.compile(r"^(CC0|Public domain|PD|CC BY(-SA)? [0-9.]+)", re.IGNORECASE) +REUSABLE_OPENVERSE = {"by", "by-sa", "cc0", "pdm"} + + +def get_json(url: str, params: dict, post: bool = False) -> dict: + encoded = urllib.parse.urlencode(params) + request = ( + urllib.request.Request(url, data=encoded.encode(), headers={"User-Agent": USER_AGENT}) + if post + else urllib.request.Request(f"{url}?{encoded}", headers={"User-Agent": USER_AGENT}) + ) + for attempt in range(4): + try: + with urllib.request.urlopen(request, timeout=60) as response: + return json.load(response) + except urllib.error.HTTPError as error: + if error.code != 429 or attempt == 3: + raise + time.sleep(30 * (attempt + 1)) + raise AssertionError("unreachable") + + +def commons(query: str) -> list[dict]: + hits = get_json( + COMMONS_API, + { + "action": "query", + "list": "search", + "srsearch": query, + "srnamespace": "6", + "srlimit": "100", + "format": "json", + "formatversion": "2", + }, + )["query"]["search"] + titles = [hit["title"] for hit in hits] + rows = [] + for start in range(0, len(titles), 50): + data = get_json( + COMMONS_API, + { + "action": "query", + "titles": "|".join(titles[start : start + 50]), + "prop": "imageinfo", + "iiprop": "url|size|mime|extmetadata", + "iiurlwidth": str(MAX_WIDTH), + "format": "json", + "formatversion": "2", + }, + post=True, + ) + for page in data["query"]["pages"]: + info = (page.get("imageinfo") or [None])[0] + if not info or info.get("mime") not in ("image/jpeg", "image/png"): + continue + meta = info.get("extmetadata", {}) + + def field(name: str, meta=meta) -> str: + value = str(meta.get(name, {}).get("value", "")) + return " ".join(re.sub(r"<[^>]+>", "", value).split()) + + if not REUSABLE_COMMONS.match(field("LicenseShortName")): + continue + wide = info["width"] > MAX_WIDTH and "thumburl" in info + rows.append( + { + "source": "wikimedia", + "title": page["title"], + "page_url": info["descriptionurl"], + "image_url": (info["thumburl"] if wide else info["url"]).split("?")[0], + "width": info["width"], + "height": info["height"], + "license": field("LicenseShortName"), + "license_url": field("LicenseUrl"), + "author": field("Artist")[:120], + } + ) + return rows + + +def openverse(query: str, pages: int = 5) -> list[dict]: + rows = [] + for page in range(1, pages + 1): + data = get_json( + OPENVERSE_API, + { + "q": query, + "license": ",".join(sorted(REUSABLE_OPENVERSE)), + "page_size": "20", + "page": str(page), + }, + ) + for item in data["results"]: + if item["license"] not in REUSABLE_OPENVERSE: + continue + license_name = f"CC {item['license'].upper()} {item.get('license_version') or ''}" + rows.append( + { + "source": item["source"], + "title": item["title"] or "", + "page_url": item["foreign_landing_url"], + "image_url": item["url"], + "width": item.get("width") or "", + "height": item.get("height") or "", + "license": license_name.strip(), + "license_url": item.get("license_url") or "", + "author": (item.get("creator") or "")[:120], + } + ) + if page >= data.get("page_count", 0): + break + time.sleep(2) + return rows + + +def find(args: argparse.Namespace) -> None: + found: dict[str, dict] = {} + for query in SEARCHES: + for row in commons(query) + openverse(query): + found.setdefault(row["page_url"], row) + print(f"{query}: {len(found)} unique so far", flush=True) + time.sleep(2) + rows = [ + r + for r in found.values() + if not r["width"] or min(int(r["width"]), int(r["height"])) >= args.min_side + ] + out = DATA_DIR / "candidates.csv" + with out.open("w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + print(f"{len(rows)} candidates -> {out}") + + +def download(url: str) -> bytes: + request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) + for attempt in range(4): + try: + with urllib.request.urlopen(request, timeout=120) as response: + return response.read() + except urllib.error.HTTPError as error: + if error.code != 429 or attempt == 3: + raise + time.sleep(30 * (attempt + 1)) + raise AssertionError("unreachable") + + +def save_upright(data: bytes, dest: Path) -> None: + """Apply EXIF rotation, cap the width, save plain RGB.""" + with Image.open(io.BytesIO(data)) as image: + upright = ImageOps.exif_transpose(image).convert("RGB") + if upright.width > MAX_WIDTH: + upright.thumbnail((MAX_WIDTH, MAX_WIDTH * 4)) + upright.save(dest, quality=95) + + +def fetch(args: argparse.Namespace) -> None: + source = Path(args.source) if args.source else MANIFEST + with source.open() as handle: + rows = list(csv.DictReader(handle)) + images = DATA_DIR / "images" + images.mkdir(parents=True, exist_ok=True) + for row in rows: + dest = images / f"{row['id']}.jpg" + if dest.exists(): + continue + save_upright(download(row["image_url"]), dest) + print(f"fetched {row['id']}", flush=True) + time.sleep(1.0) + print(f"{len(rows)} images in {images}") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(required=True) + p_find = sub.add_parser("find", help="list reusable candidate photos") + p_find.add_argument("--min-side", type=int, default=800) + p_find.set_defaults(func=find) + p_fetch = sub.add_parser("fetch", help="download the photos in manifest.csv") + p_fetch.add_argument( + "--source", help="CSV with id and image_url columns (default manifest.csv)" + ) + p_fetch.set_defaults(func=fetch) + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/experiments/panel-label/uv.lock b/experiments/panel-label/uv.lock new file mode 100644 index 000000000..8b2da4ea6 --- /dev/null +++ b/experiments/panel-label/uv.lock @@ -0,0 +1,333 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/50/8fdbb16af64895706a45f06a4068e29db732ec180f3c1375f14123359138/numpy-2.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb189f09db39283b26bfd061ec16189e14f71c6755207f72a0f7540867afe5b9", size = 16994982, upload-time = "2026-09-06T16:24:29.244Z" }, + { url = "https://files.pythonhosted.org/packages/60/39/789131c1188c078dcb3a1692e72e1e050c68b88ffe72c9ccaac9bcd7a9cd/numpy-2.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f59a878c33d6b88122d80d239bb3b845d58708750b0cb06a09aebb9b18ec696c", size = 12009327, upload-time = "2026-09-06T16:24:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/a312e95696e5f601914dd8b6dd844692ba61670807417e24b68e337b5c70/numpy-2.5.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a72f874bc9e10e4b8f80426fb49716d5141f64442a0c8418065093ec8017fbb0", size = 5445405, upload-time = "2026-09-06T16:24:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/30/d0/5623a1707ed4fe16e3909fe3cf5ee3da004ae677ad23d83bbf3adf1a6faf/numpy-2.5.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fc36dc566135b5eceec4cf89758fcb719266a019ef07dae1754ae7c9f617ef3e", size = 6783213, upload-time = "2026-09-06T16:24:37.253Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/84146fc020ad3c25f805f70ab60da46fe3c540a21369754a7e4369754b6f/numpy-2.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c2c1e6bfa5c84adc6434dfbf013aa92096a7985221762c8f11fedfd20fff58", size = 15687872, upload-time = "2026-09-06T16:24:39.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/aa78d1a88805456e212b65461354cd943197fb9acecc4c90fd12295123a3/numpy-2.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7e18c623bb5c95acb3b3328861272816ba199fb531921c5d6d0b675f1fde9e3", size = 16717410, upload-time = "2026-09-06T16:24:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/24/faa79d865e69a97ba17473b23a1b74094b2259c03e820c70297293b9ea49/numpy-2.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f8929ee6c96bfbd7b4ed2032e0c03af86fe1826740ab61ddabf9072d06e57ff", size = 17040975, upload-time = "2026-09-06T16:24:45.961Z" }, + { url = "https://files.pythonhosted.org/packages/62/4a/8877e629445a7176297dffcaf9c485faa96a95d81728a62521ad55bd4c0f/numpy-2.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b5d93cf48f687479941d12b69c873ad2cc76bbd487f0091c2200636497f34034", size = 18476479, upload-time = "2026-09-06T16:24:49.35Z" }, + { url = "https://files.pythonhosted.org/packages/c8/db/35e1c2d38b04cbd5b731f9d71495e055e813197669d22b612f11748d2ff9/numpy-2.5.3-cp312-cp312-win32.whl", hash = "sha256:bf63afbe037eb5d2fe87fbcc7778e61da53ebaf21d938a4515aa73b62532a5d4", size = 6133378, upload-time = "2026-09-06T16:24:51.915Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/accf6d4f0c80c5d9ba9735d6b1550e444180599f34dec69ca01360f717ad/numpy-2.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:0a59a421a32580a009e8a1751345bf829631b990dc1794b80514ab722b435def", size = 12567828, upload-time = "2026-09-06T16:24:54.255Z" }, + { url = "https://files.pythonhosted.org/packages/22/43/1764aff32e4652526ae2f71fa8b3efd8d25c8a3d6926914454e47138ed1e/numpy-2.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:ccb32e0525d29e8b0572eb84c9a57af0e7a4e615726927506f55063c62414034", size = 10485432, upload-time = "2026-09-06T16:24:57.278Z" }, + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "panel-label" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "scipy" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=2.1" }, + { name = "pillow", specifier = ">=11.0" }, + { name = "scipy", specifier = ">=1.14" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3" }, + { name = "ruff", specifier = ">=0.8" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" }, + { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" }, + { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" }, + { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" }, + { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" }, + { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" }, + { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/f7/240c110c08693826b4513a52f5717d62ec7c7af72f2920821247c03b17b3/scipy-1.18.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:457fd7a2a8edeb044ab6ffbc0aa03ff6cd18491356e5e0c834d76ce621b916d1", size = 31111061, upload-time = "2026-08-21T23:23:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/05/4a/78c6285577c375e7cf27277ea8ee6961224327f1e1a0c44af5f17f23635c/scipy-1.18.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e708533e8b2ae2497d65346538a7dcc92814410b25b81432eac66de0f2af8265", size = 28733332, upload-time = "2026-08-21T23:23:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f6/a5b82f8abbe14d134691b8b903696f701d25a081353a29dc655c364d9e62/scipy-1.18.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7bbf207c4453ce1ad2e00b17313852b33310b83090c2311bdaf97f93c0380d12", size = 20475078, upload-time = "2026-08-21T23:23:54.138Z" }, + { url = "https://files.pythonhosted.org/packages/23/22/0858a0bbd6b3e825ceb8cd9baf9eaf3b2f2b1d77727eb6be40500bcdc92f/scipy-1.18.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:78c0665edead396b1abb4897c41a5c1d9bf090c8a637a4c20a61678e0a264e66", size = 23108904, upload-time = "2026-08-21T23:23:57.824Z" }, + { url = "https://files.pythonhosted.org/packages/75/9a/2e71719f31eaefe0e3a1706c4a1ded94e664bfd95ffca2b219a671faee01/scipy-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c085faa2cfa879c5141df483f836f4d691045a078224a670fa570fa01612d89", size = 34025113, upload-time = "2026-08-21T23:24:02.209Z" }, + { url = "https://files.pythonhosted.org/packages/df/64/ff35eb9e54894cf471ff4716abd3c81eb0a0626869217ce3e6ba4ccf17d7/scipy-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f55fa87b6c612ecd6b058f167c53231b1d14e412efe361d3d6e38b3631c73218", size = 35344199, upload-time = "2026-08-21T23:24:07.844Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/c5538be1792f7034c12c7db6ee67cace58253c7b87b122d68253eaf5de89/scipy-1.18.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c35d74ce0e193ff740c2f2be2ac913ddc232fe6c1ff40b26cfecb9c670c63314", size = 35639587, upload-time = "2026-08-21T23:24:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/91/4c/075e4f66471bac101141ac739e9e135549be1bae584571bd03a530c056e1/scipy-1.18.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2924a03db38dc2e848bca2fe9f077dafb891480b91a00a0963a8cf86dfc31c1", size = 37480330, upload-time = "2026-08-21T23:24:19.608Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/979fd14e75008623df31ba70d6bb144700f68feadcea042021c06a05bf82/scipy-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:5e4d44984abc0020154ea81b247adeddcc3ac5527b975ff798bd1ba0adc513c2", size = 36658278, upload-time = "2026-08-21T23:24:25.463Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/e1525354ff9d7d5feb6d1b31af6d14072e5c91e9607b421fa1ec889660b3/scipy-1.18.1-cp312-cp312-win_arm64.whl", hash = "sha256:d65d448389b8436493abcf629cc94ad0cf32aecaf06e1acca1de53cc795f2f12", size = 24400588, upload-time = "2026-08-21T23:24:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" }, + { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" }, + { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" }, + { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" }, + { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" }, + { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" }, +] From 309e6e0a3b5968d3313cef670bf1975ad5414c54 Mon Sep 17 00:00:00 2001 From: SamGu-NRX Date: Sat, 26 Sep 2026 07:34:32 -0500 Subject: [PATCH 2/4] experiments(panel-label): too few openly licensed panel-label photos; record the search Screens about 2,000 Commons and Openverse results; 67 photos examined at full resolution have 19 with any label field legible, against a pre-set bar of about 40, so the reading, naming and transfer questions are not measured. Adds the screening record, a tally command, tests and a path-filtered CI job. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/panel-label.yml | 46 +++++++++++ experiments/panel-label/README.md | 42 +++++++++- experiments/panel-label/screened.csv | 79 +++++++++++++++++++ .../panel-label/src/panel_eval/screen.py | 48 +++++++++++ experiments/panel-label/tests/test_panel.py | 52 ++++++++++++ 5 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/panel-label.yml create mode 100644 experiments/panel-label/screened.csv create mode 100644 experiments/panel-label/src/panel_eval/screen.py create mode 100644 experiments/panel-label/tests/test_panel.py diff --git a/.github/workflows/panel-label.yml b/.github/workflows/panel-label.yml new file mode 100644 index 000000000..0917bf8a6 --- /dev/null +++ b/.github/workflows/panel-label.yml @@ -0,0 +1,46 @@ +name: Panel label experiment + +on: + push: + branches: [main] + paths: + - "experiments/panel-label/**" + - ".github/workflows/panel-label.yml" + pull_request: + paths: + - "experiments/panel-label/**" + - ".github/workflows/panel-label.yml" + +permissions: + contents: read + +concurrency: + group: panel-label-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + checks: + name: Panel label checks + runs-on: ubuntu-24.04 + timeout-minutes: 10 + defaults: + run: + working-directory: experiments/panel-label + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 + with: + working-directory: experiments/panel-label + - run: uv sync --locked + - run: uv run ruff check . + - run: uv run ruff format --check . + - run: uv run pytest -q + # Photos of real panels can show addresses and inspection stickers; they stay out of git. + - name: No photos committed + run: | + if git ls-files . | grep -iE '\.(jpe?g|png|heic|webp|tiff?)$'; then + echo "Photos must stay in ~/house-scanning-data/panel, not in the repository." >&2 + exit 1 + fi diff --git a/experiments/panel-label/README.md b/experiments/panel-label/README.md index 6cd2628af..1ca1f03a4 100644 --- a/experiments/panel-label/README.md +++ b/experiments/panel-label/README.md @@ -1,6 +1,6 @@ # Panel label readability -Installers check the main electrical panel's manufacturer, model and main breaker amperage. Some panel lines are known hazards: Federal Pacific Stab-Lok and Zinsco are both subjects of public safety notices. The main breaker's amperage matters for any battery install. If the phone can read a close-up of the panel label, a homeowner can answer with one photo instead of a form. This experiment measures that on real, openly licensed panel photos, reusing the meter close-up tooling. +Installers check the main electrical panel's manufacturer, model and main breaker amperage. Some panel lines are known hazards: Federal Pacific Stab-Lok and Zinsco are both subjects of public safety notices. The main breaker's amperage matters for any battery install. If the phone can read a close-up of the panel label, a homeowner can answer with one photo instead of a form. This experiment set out to measure that on real, openly licensed panel photos, reusing the meter close-up tooling. ## Questions and pass criteria, set before the run @@ -9,3 +9,43 @@ Installers check the main electrical panel's manufacturer, model and main breake 3. Do the meter close-up's blur and too-small checks transfer to panel labels? They transfer if, on the same controlled degradations of the panel photos, the panel's own 95% break-point threshold for each check is no stricter than the meter's (whole-photo sharpness 6.63; top-candidate line height 33.9 px). Otherwise the panel needs its own thresholds, which are reported. If fewer than about 40 usable openly licensed photos exist, the experiment stops there and reports what was searched. + +## Result: stopped, too few photos + +There are not enough openly licensed photos of residential panel labels to answer these questions. None of the three was measured. + +| Photos examined at full resolution, duplicates removed | 67 | +|---|---| +| Any of manufacturer, model or amperage legible | 19 | +| Manufacturer and a model or amperage legible | 9 | +| All three legible | 6 | + +`uv run python -m panel_eval.screen` regenerates these counts from `screened.csv`, which lists every examined photo with its URL, license, author and the per-field call. The calls were made by one AI reader (Claude Opus) looking at each photo at its stored resolution, so treat the counts as approximate. Even so, the lenient count is under half the 40-photo bar. The 9 photos also overstate the pool: two show the same panel, and one is an Indian moulded-case breaker, not a US residential panel. That leaves 7 distinct US panels. + +The known-hazard lines are almost absent: one Federal Pacific Stab-Lok panel, found twice, and no Zinsco panel. Precision and recall for naming a hazard line cannot be estimated from one example. + +As an anecdote and not a measured rate: on the 8 legible US photos, Vision read the manufacturer or line name on 6 (Federal Pacific and Stab-Lok, Frank Adam, Square D QO twice, GE, Square D). It read the amperage text on 4 ("100", "100A", "Mains 100 A Max", "125"). It read nothing useful on the Siemens main breaker, and garbled the Square D Multi-Breaker plate. + +## What was searched + +Between 2026-09-26 06:00 and 07:30, filtered to CC0, public domain, CC BY and CC BY-SA: + +- **Wikimedia Commons.** The file search for each of 20 queries in `src/panel_eval/sources.py` (for example "breaker panel", "load center", "Federal Pacific", "Stab-Lok", "Zinsco", plus maker names with "breaker panel"), and the `Square D` and `Square D circuit breakers` categories. There are no Commons categories for US residential panels, load centers or breaker boxes; the related categories (`Distribution boards`, `Circuit breakers`) hold mostly European DIN-rail boards and industrial gear. +- **Openverse** (openly licensed images indexed from Flickr, Commons and others): the same 20 queries at up to 100 results each, then every accessible page of 11 core queries, 1,808 results in total. The anonymous API stops at 240 results per query, so the long tails of "electrical panel", "fuse box", "load center", "circuit breaker" and "service panel" were not reachable. + +After de-duplication there were about 2,000 unique results. Screening titles and thumbnails left 116 candidates; after removing duplicates and non-panels, 67 were examined at full resolution. Most results are aircraft and ship panels, European consumer units, industrial switchgear, substations, car fuse boxes and diagrams. Most US residential panel photos are wide shots of wiring in which no label text is legible; Flickr copies in Openverse are mostly 1024 px. + +## What would unblock it + +- **Photos the team takes or collects locally.** Like the meter photos, these would stay outside git. Ten minutes per house covers a labelled deadfront, the door label and the main breaker. About 40 panels would support the pre-set criteria, and a few older homes would cover the hazard lines. +- **Sources this search did not use.** Unsplash and Pexels photos are free to use but not CC-licensed, and whether they count as "openly licensed" is the team's call. US government works are public domain, for example CPSC recall notices, but those show individual breakers rather than panel labels. Home-inspection sites have many panel photos but are not openly licensed. + +## Rerun + +```sh +uv run python -m panel_eval.sources find # candidates.csv in ~/house-scanning-data/panel +uv run python -m panel_eval.screen # the counts above, from screened.csv +uv run pytest -q +``` + +`meterocr/` is a copy of the meter experiment's Vision reader, kept so this folder stands alone once the team has photos. diff --git a/experiments/panel-label/screened.csv b/experiments/panel-label/screened.csv new file mode 100644 index 000000000..843a1b336 --- /dev/null +++ b/experiments/panel-label/screened.csv @@ -0,0 +1,79 @@ +id,source,title,page_url,image_url,license,license_url,author,duplicate_of,manufacturer,model,amperage,note +p01,wikimedia,File:Air duct and fuse box in a construction site.jpg,https://commons.wikimedia.org/wiki/File:Air_duct_and_fuse_box_in_a_construction_site.jpg,https://upload.wikimedia.org/wikipedia/commons/1/1c/Air_duct_and_fuse_box_in_a_construction_site.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0,"Paul Cutler from Chaska, USA",,0,0,0, +p02,wikimedia,File:Eaton circuit breaker panel open.JPG,https://commons.wikimedia.org/wiki/File:Eaton_circuit_breaker_panel_open.JPG,https://upload.wikimedia.org/wikipedia/commons/6/62/Eaton_circuit_breaker_panel_open.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,BrokenSphere,,1,0,0,"Eaton door label, fine print not legible" +p03,wikimedia,File:Fuse box in old apartment building.JPG,https://commons.wikimedia.org/wiki/File:Fuse_box_in_old_apartment_building.JPG,https://upload.wikimedia.org/wikipedia/commons/f/f1/Fuse_box_in_old_apartment_building.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,Dmitry G,,0,0,0, +p04,wikimedia,File:Eaton circuit breaker panel closed.JPG,https://commons.wikimedia.org/wiki/File:Eaton_circuit_breaker_panel_closed.JPG,https://upload.wikimedia.org/wikipedia/commons/e/ec/Eaton_circuit_breaker_panel_closed.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,BrokenSphere,,1,0,0,"Eaton logo on closed door, small" +p05,wikimedia,File:Nice USSR fuse box.JPG,https://commons.wikimedia.org/wiki/File:Nice_USSR_fuse_box.JPG,https://upload.wikimedia.org/wikipedia/commons/c/c9/Nice_USSR_fuse_box.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,Dmitry G,,0,0,0, +p06,wikimedia,File:Murray-fuse-box.jpg,https://commons.wikimedia.org/wiki/File:Murray-fuse-box.jpg,https://upload.wikimedia.org/wikipedia/commons/b/b0/Murray-fuse-box.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0,"Original photo: p.Gordon on Flickr Modified by Gazebo on June 1, 2014 with cropping, lossless optimi",,0,0,0, +p07,wikimedia,File:Stab-Lok circuit breaker panel interior .jpg,https://commons.wikimedia.org/wiki/File:Stab-Lok_circuit_breaker_panel_interior_.jpg,https://upload.wikimedia.org/wikipedia/commons/1/19/Stab-Lok_circuit_breaker_panel_interior_.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0,Repeater-reclaim,,1,1,1,Federal Pacific Stab-Lok load center; main handle 100 +p08,wikimedia,File:EMU100 Breaker Panel EP101 DSC 0886.jpg,https://commons.wikimedia.org/wiki/File:EMU100_Breaker_Panel_EP101_DSC_0886.jpg,https://upload.wikimedia.org/wikipedia/commons/f/f1/EMU100_Breaker_Panel_EP101_DSC_0886.jpg,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,Lexcie,,0,0,0, +p09,wikimedia,"File:Жаркент, дунганская мечеть, щитки.jpg","https://commons.wikimedia.org/wiki/File:%D0%96%D0%B0%D1%80%D0%BA%D0%B5%D0%BD%D1%82,_%D0%B4%D1%83%D0%BD%D0%B3%D0%B0%D0%BD%D1%81%D0%BA%D0%B0%D1%8F_%D0%BC%D0%B5%D1%87%D0%B5%D1%82%D1%8C,_%D1%89%D0%B8%D1%82%D0%BA%D0%B8.jpg",https://thumb.wikimedia.org/wikipedia/commons/thumb/5/59/%D0%96%D0%B0%D1%80%D0%BA%D0%B5%D0%BD%D1%82%2C_%D0%B4%D1%83%D0%BD%D0%B3%D0%B0%D0%BD%D1%81%D0%BA%D0%B0%D1%8F_%D0%BC%D0%B5%D1%87%D0%B5%D1%82%D1%8C%2C_%D1%89%D0%B8%D1%82%D0%BA%D0%B8.jpg/3840px-%D0%96%D0%B0%D1%80%D0%BA%D0%B5%D0%BD%D1%82%2C_%D0%B4%D1%83%D0%BD%D0%B3%D0%B0%D0%BD%D1%81%D0%BA%D0%B0%D1%8F_%D0%BC%D0%B5%D1%87%D0%B5%D1%82%D1%8C%2C_%D1%89%D0%B8%D1%82%D0%BA%D0%B8.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0,Nikolai Bulykin,,0,0,0, +p10,wikimedia,EMU100 Breaker Panel EP101 DSC 0886,https://commons.wikimedia.org/w/index.php?curid=115094472,https://upload.wikimedia.org/wikipedia/commons/f/f1/EMU100_Breaker_Panel_EP101_DSC_0886.jpg,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0/,Lexcie,p08,0,0,0, +p11,wikimedia,Stab-Lok circuit breaker panel interior,https://commons.wikimedia.org/w/index.php?curid=108022261,https://upload.wikimedia.org/wikipedia/commons/1/19/Stab-Lok_circuit_breaker_panel_interior_.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0/,Repeater-reclaim,p07,0,0,0, +p12,wikimedia,Schneider Electric 1-Phase 100-Amp Residential Breaker Panel,https://commons.wikimedia.org/w/index.php?curid=25772432,https://upload.wikimedia.org/wikipedia/commons/1/1d/Schneider_Electric_1-Phase_100-Amp_Residential_Breaker_Panel.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0/,Kevin Kiefuik,,1,0,0,"Schneider Electric label on side, fine print not legible" +p13,wikimedia,Eaton circuit breaker panel open,https://commons.wikimedia.org/w/index.php?curid=3898593,https://upload.wikimedia.org/wikipedia/commons/6/62/Eaton_circuit_breaker_panel_open.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0/,BrokenSphere,p02,0,0,0, +p14,wikimedia,Eaton circuit breaker panel closed,https://commons.wikimedia.org/w/index.php?curid=3898597,https://upload.wikimedia.org/wikipedia/commons/e/ec/Eaton_circuit_breaker_panel_closed.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0/,BrokenSphere,p04,0,0,0, +p15,wikimedia,Electrical panel and subpanel with cover removed from subpanel,https://commons.wikimedia.org/w/index.php?curid=28566920,https://upload.wikimedia.org/wikipedia/commons/e/ef/Electrical_panel_and_subpanel_with_cover_removed_from_subpanel.jpg,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0/,Gvolk,,0,0,0, +p16,wikimedia,Electrical panels in 1946 house,https://commons.wikimedia.org/w/index.php?curid=119120895,https://upload.wikimedia.org/wikipedia/commons/a/a3/Electrical_panels_in_1946_house.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0/,Circacies,,1,0,0,"Frank Adam badge on deadfront, small" +p17,wikimedia,File:OpenClosedPanelboardEEUU(GE)CloseupTop.jpg,https://commons.wikimedia.org/wiki/File:OpenClosedPanelboardEEUU(GE)CloseupTop.jpg,https://upload.wikimedia.org/wikipedia/commons/e/ef/OpenClosedPanelboardEEUU%28GE%29CloseupTop.jpg,Public domain,,Where at English Wikipedia,,0,0,1,main handle 100; no maker text +p18,wikimedia,File:Wall Circuit Breaker and Wiring.jpg,https://commons.wikimedia.org/wiki/File:Wall_Circuit_Breaker_and_Wiring.jpg,https://upload.wikimedia.org/wikipedia/commons/5/55/Wall_Circuit_Breaker_and_Wiring.jpg,CC0,http://creativecommons.org/publicdomain/zero/1.0/deed.en,Paul The Writer,,0,0,0, +p19,wikimedia,US wiring basement-panel,https://commons.wikimedia.org/w/index.php?curid=25730113,https://upload.wikimedia.org/wikipedia/commons/5/5d/US_wiring_basement-panel.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,0,0,0, +p20,wikimedia,Electrical panel opened,https://commons.wikimedia.org/w/index.php?curid=112407360,https://upload.wikimedia.org/wikipedia/commons/6/65/Electrical_panel_opened.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0/,FuriousYogi,,1,0,0,"Eaton door label, fine print not legible" +p21,wikimedia,File:Seattle - Moore Theatre - electricals 02.jpg,https://commons.wikimedia.org/wiki/File:Seattle_-_Moore_Theatre_-_electricals_02.jpg,https://upload.wikimedia.org/wikipedia/commons/8/81/Seattle_-_Moore_Theatre_-_electricals_02.jpg,CC BY-SA 3.0,http://creativecommons.org/licenses/by-sa/3.0/,Joe Mabel.,,0,0,0, +p22,wikimedia,File:EEUU(GE)BreakerpanelInnards.jpg,https://commons.wikimedia.org/wiki/File:EEUU(GE)BreakerpanelInnards.jpg,https://upload.wikimedia.org/wikipedia/commons/e/ea/EEUU%28GE%29BreakerpanelInnards.jpg,Public domain,,The original uploader was Where at English Wikipedia.,,0,0,0, +p23,wikimedia,File:US wiring basement-panel.jpg,https://commons.wikimedia.org/wiki/File:US_wiring_basement-panel.jpg,https://upload.wikimedia.org/wikipedia/commons/5/5d/US_wiring_basement-panel.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0,davef3138,p19,0,0,0, +p24,wikimedia,File:Electrical panel and subpanel with cover removed from subpanel.jpg,https://commons.wikimedia.org/wiki/File:Electrical_panel_and_subpanel_with_cover_removed_from_subpanel.jpg,https://upload.wikimedia.org/wikipedia/commons/e/ef/Electrical_panel_and_subpanel_with_cover_removed_from_subpanel.jpg,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,Gvolk,p15,0,0,0, +p25,wikimedia,File:Electrical panel opened.jpg,https://commons.wikimedia.org/wiki/File:Electrical_panel_opened.jpg,https://upload.wikimedia.org/wikipedia/commons/6/65/Electrical_panel_opened.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0,FuriousYogi,p20,0,0,0, +p26,wikimedia,File:Exposed Wall Wiring and Breaker Box.jpg,https://commons.wikimedia.org/wiki/File:Exposed_Wall_Wiring_and_Breaker_Box.jpg,https://thumb.wikimedia.org/wikipedia/commons/thumb/1/18/Exposed_Wall_Wiring_and_Breaker_Box.jpg/3840px-Exposed_Wall_Wiring_and_Breaker_Box.jpg,CC0,http://creativecommons.org/publicdomain/zero/1.0/deed.en,Paul The Writer,,0,0,0, +p27,wikimedia,File:Fuse box grounded to hot wire.JPG,https://commons.wikimedia.org/wiki/File:Fuse_box_grounded_to_hot_wire.JPG,https://upload.wikimedia.org/wikipedia/commons/e/e5/Fuse_box_grounded_to_hot_wire.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,Dmitry G,,0,0,0, +p28,flickr,HipstaPrint Circuit Breaker Box,https://www.flickr.com/photos/10710442@N08/18787764101,https://live.staticflickr.com/364/18787764101_47939ef1ef_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Steve Snodgrass,,0,0,0, +p29,wikimedia,File:Distribution board in Slovakia 01.jpg,https://commons.wikimedia.org/wiki/File:Distribution_board_in_Slovakia_01.jpg,https://thumb.wikimedia.org/wikipedia/commons/thumb/0/0a/Distribution_board_in_Slovakia_01.jpg/3840px-Distribution_board_in_Slovakia_01.jpg,CC BY 4.0,https://creativecommons.org/licenses/by/4.0,Oto Zapletal,,0,0,0, +p30,wikimedia,File:Old Soviet distribution board.JPG,https://commons.wikimedia.org/wiki/File:Old_Soviet_distribution_board.JPG,https://upload.wikimedia.org/wikipedia/commons/9/9d/Old_Soviet_distribution_board.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,Pinot,,0,0,0, +p31,flickr,Abandoned Dairy Factory (8),https://www.flickr.com/photos/30036111@N04/8170430415,https://live.staticflickr.com/8197/8170430415_e03ca117e4_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,janbommes,,0,0,0, +p32,wikimedia,File:Electrical panels in 1946 house.jpg,https://commons.wikimedia.org/wiki/File:Electrical_panels_in_1946_house.jpg,https://upload.wikimedia.org/wikipedia/commons/a/a3/Electrical_panels_in_1946_house.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0,Circacies,p16,0,0,0, +p33,wikimedia,File:Square D main circuit breaker from 1946.jpg,https://commons.wikimedia.org/wiki/File:Square_D_main_circuit_breaker_from_1946.jpg,https://upload.wikimedia.org/wikipedia/commons/f/f6/Square_D_main_circuit_breaker_from_1946.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0,Circacies,,1,1,0,Square D Multi-Breaker main; handle rating too small to read +p34,wikimedia,File:Square D 20-Amp Double-Pole Breaker (1).JPG,https://commons.wikimedia.org/wiki/File:Square_D_20-Amp_Double-Pole_Breaker_(1).JPG,https://upload.wikimedia.org/wikipedia/commons/6/65/Square_D_20-Amp_Double-Pole_Breaker_%281%29.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,CyberXRef,,0,0,0, +p35,wikimedia,File:Square D 20-Amp Single-Pole Breaker (1).JPG,https://commons.wikimedia.org/wiki/File:Square_D_20-Amp_Single-Pole_Breaker_(1).JPG,https://upload.wikimedia.org/wikipedia/commons/c/cf/Square_D_20-Amp_Single-Pole_Breaker_%281%29.JPG,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0,CyberXRef,,0,0,0, +p36,flickr,Main house breaker and old meter,https://www.flickr.com/photos/35034363486@N01/1462085618,https://live.staticflickr.com/1247/1462085618_5918e51aab_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Mike Weston,,0,0,0, +p37,wikimedia,Frank Adam electrical panel label from 1946,https://commons.wikimedia.org/w/index.php?curid=119120896,https://upload.wikimedia.org/wikipedia/commons/f/f1/Frank_Adam_electrical_panel_label_from_1946.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0/,Circacies,,1,1,1,"Frank Adam load center nameplate, 100 A, catalogue number" +p38,wordpress,"Close-up of a 200-amp breaker in the ON position, with red, black, blue, yellow, and brown wires con",https://wordpress.org/photos/photo/515687aa4e/,https://pd.w.org/2025/07/515687aa4ebe1f8c3.25750924-2048x1152.jpg,CC CC0 1.0,https://creativecommons.org/publicdomain/zero/1.0/,Moses Cursor Ssebunya,,1,0,1,"Indian 200 A moulded-case main breaker, not a US residential panel" +p39,flickr,Electrical panel finished,https://www.flickr.com/photos/34197011@N00/202290783,https://live.staticflickr.com/70/202290783_65a8eead40_b.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,scottbb,,0,0,0, +p40,flickr,Electrical closeup,https://www.flickr.com/photos/34197011@N00/202290560,https://live.staticflickr.com/71/202290560_fb40def5b7_b.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,scottbb,,1,0,1,"Siemens main breaker, 150 A" +p41,flickr,Megger testing the main and branch breakers in electrical panels in a power substation in Queens. 09,https://www.flickr.com/photos/59595815@N03/48689467532,https://live.staticflickr.com/65535/48689467532_8dcb145f84_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,MTA C&D - EAST SIDE ACCESS,,0,0,0, +p42,flickr,Electrical hazard in old panel,https://www.flickr.com/photos/26670433@N07/27652554691,https://live.staticflickr.com/7446/27652554691_91a0a15969_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,0,0,0, +p43,flickr,200A main panel,https://www.flickr.com/photos/26670433@N07/36167061425,https://live.staticflickr.com/4315/36167061425_09b0334565_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,0,0,0, +p44,flickr,Mains switch/breaker panel...,https://www.flickr.com/photos/91198056@N00/8295829115,https://live.staticflickr.com/8076/8295829115_a0ac42fbf9.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Podknox,,0,0,0, +p45,wikimedia,Megger testing the main and branch breakers in electrical panels in a power substation in Queens. 09,https://commons.wikimedia.org/w/index.php?curid=86620496,https://upload.wikimedia.org/wikipedia/commons/6/62/Megger_testing_the_main_and_branch_breakers_in_electrical_panels_in_a_power_substation_in_Queens._09-04-2019_%2848689467532%29.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,MTA Capital Construction Mega Projects,p41,0,0,0, +p46,flickr,Preparation for injection testing after an electrical panel upgrade. 06-25-19,https://www.flickr.com/photos/59595815@N03/48146653086,https://live.staticflickr.com/65535/48146653086_50528e0cf2_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,MTA C&D - EAST SIDE ACCESS,,0,0,0, +p47,flickr,Electrical panel upgrade,https://www.flickr.com/photos/26670433@N07/27161367425,https://live.staticflickr.com/7342/27161367425_0202dfd83c_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,0,0,0, +p48,flickr,1 electrical panel upgrade before - Copy,https://www.flickr.com/photos/62485517@N08/13018082643,https://live.staticflickr.com/7366/13018082643_b2bbe0a94d.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,fallsavenuevintagefashion,,0,0,0, +p49,flickr,2 electrical panel upgrade after - Copy,https://www.flickr.com/photos/62485517@N08/13017916415,https://live.staticflickr.com/7410/13017916415_b4b638122e.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,fallsavenuevintagefashion,,0,0,0, +p50,flickr,Electrical Panel upgrade,https://www.flickr.com/photos/193473686@N05/52584053369,https://live.staticflickr.com/65535/52584053369_2f99fc876f_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Phyxter Home Services,,0,0,0, +p51,flickr,Performing injection testing on the electrical breakers after a panel upgrade in a power substation.,https://www.flickr.com/photos/59595815@N03/48105050226,https://live.staticflickr.com/65535/48105050226_1fafc2c9fe_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,MTA C&D - EAST SIDE ACCESS,,0,0,0, +p52,flickr,Electric service upgrade,https://www.flickr.com/photos/26670433@N07/27652552351,https://live.staticflickr.com/7687/27652552351_07da08535b_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,0,0,0, +p53,flickr,Electric service upgrade,https://www.flickr.com/photos/26670433@N07/27652555241,https://live.staticflickr.com/7249/27652555241_a39d5d9ea3_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,0,0,0, +p54,flickr,Upstairs MR breaker panel,https://www.flickr.com/photos/21881956@N05/3726676241,https://live.staticflickr.com/2555/3726676241_fc8c1fb85a_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,vaxomatic,,0,0,0, +p55,flickr,2012-09-05_040504_ac-breaker-panel,https://www.flickr.com/photos/62485517@N08/8453699897,https://live.staticflickr.com/8088/8453699897_9d6756767b_m.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,fallsavenuevintagefashion,,0,0,0, +p56,flickr,20051008main-panel-lower,https://www.flickr.com/photos/26670433@N07/3573729611,https://live.staticflickr.com/3621/3573729611_aa19d11b16_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,1,0,0,Square D logos on breakers only +p57,flickr,Old panel,https://www.flickr.com/photos/26670433@N07/27573940545,https://live.staticflickr.com/7453/27573940545_3463f959d5_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,davef3138,,0,0,0, +p58,wikimedia,File:Breakerpanel-level4-inside-view135dl.jpg,https://commons.wikimedia.org/w/index.php?curid=47145303,https://upload.wikimedia.org/wikipedia/commons/c/cf/Breakerpanel-level4-inside-view135dl.jpg,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0/,The original uploader was Glogger at English Wikipedia.,,0,0,0, +p59,flickr,Snow-filled Electrical Panel,https://www.flickr.com/photos/57202181@N05/5905086734,https://live.staticflickr.com/5277/5905086734_016c63fcf9_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Alan R. Light,,0,0,0, +p60,flickr,"Day Fifteen, Electrical Panel",https://www.flickr.com/photos/37663331@N00/2587104216,https://live.staticflickr.com/3029/2587104216_81b497106a_b.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,Numinosity by Gary J Wood,,0,0,0, +p61,flickr,Old Electrical Panel,https://www.flickr.com/photos/18658635@N00/15391420009,https://live.staticflickr.com/3956/15391420009_e0926596df_b.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,Patty OK,,0,0,0, +p62,flickr,Old electric panel in my backyard,https://www.flickr.com/photos/65588072@N00/126123291,https://live.staticflickr.com/53/126123291_4418fea272_b.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,thomascrenshaw,,0,0,0, +p63,flickr,Electric Panel in Shed,https://www.flickr.com/photos/79155290@N06/48990697453,https://live.staticflickr.com/65535/48990697453_ec4549e3a2_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,bobcapra,,1,1,1,"Square D QO load center label, mains 100 A" +p64,flickr,Electric Panel in Shed,https://www.flickr.com/photos/79155290@N06/48990697533,https://live.staticflickr.com/65535/48990697533_09bc11eca2_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,bobcapra,,1,1,1,"same panel as p63, wider view" +p65,wikimedia,"Antarctica electrical panel, wired",https://commons.wikimedia.org/w/index.php?curid=25730249,https://upload.wikimedia.org/wikipedia/commons/d/da/Antarctica_electrical_panel%2C_wired.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,Eli Duke,,0,0,0, +p66,wikimedia,Instructions on an electric panel in an apartment,https://commons.wikimedia.org/w/index.php?curid=71311202,https://upload.wikimedia.org/wikipedia/commons/4/4c/Instructions_on_an_electric_panel_in_an_apartment.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0/,Dquai,,1,1,1,"GE load center wiring-diagram label, 125 A" +p67,flickr,Painting near breaker box #1,https://www.flickr.com/photos/68552389@N00/642732090,https://live.staticflickr.com/1298/642732090_2822a28e7c.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,cbb4104,,0,0,0, +p68,flickr,breaker box,https://www.flickr.com/photos/14381622@N00/10386983003,https://live.staticflickr.com/3773/10386983003_286abfac51.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,JSF539,,0,0,0, +p69,flickr,Super Breakers,https://www.flickr.com/photos/24192350@N03/4383213682,https://live.staticflickr.com/4040/4383213682_9b5c59d599_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,StickerGiant,,0,0,0, +p70,wikimedia,Seattle - Moore Theatre - electricals 02,https://commons.wikimedia.org/w/index.php?curid=3247946,https://upload.wikimedia.org/wikipedia/commons/8/81/Seattle_-_Moore_Theatre_-_electricals_02.jpg,CC BY-SA 3.0,https://creativecommons.org/licenses/by-sa/3.0/,Joe Mabel.,p21,0,0,0, +p71,flickr,Fuse box,https://www.flickr.com/photos/64401168@N00/4723843707,https://live.staticflickr.com/1201/4723843707_03e8fe7cde_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Average Jane,,1,1,1,"Square D switch and fuse cabinet nameplate, 60 A" +p72,flickr,Fuse box,https://www.flickr.com/photos/69024677@N00/2355953595,https://live.staticflickr.com/2101/2355953595_6541da1784_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Giles Douglas,,0,0,0, +p73,flickr,Overkill fuse box,https://www.flickr.com/photos/69024677@N00/2408454318,https://live.staticflickr.com/3272/2408454318_04fafa9520_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Giles Douglas,,0,0,0, +p74,flickr,Fuse box w/o permit and missing labels,https://www.flickr.com/photos/14529688@N08/6164685732,https://live.staticflickr.com/6168/6164685732_6ee8277d7f_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Ken_Mayer,,1,0,0,GE logo on door label +p75,flickr,Circuit breakers,https://www.flickr.com/photos/44124483743@N01/3957069731,https://live.staticflickr.com/2491/3957069731_055b58eda8_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,brownpau,,1,0,0,"Square D door label, fine print not legible at 1024 px" +p76,flickr,Circuit Breaker,https://www.flickr.com/photos/49062413@N00/3354702763,https://live.staticflickr.com/3550/3354702763_a5a814c903_b.jpg,CC BY 2.0,https://creativecommons.org/licenses/by/2.0/,Helga's Lobster Stew,,1,0,0,"Westinghouse 15 A breaker plate, not a main" +p77,wikimedia,Square D main circuit breaker from 1946,https://commons.wikimedia.org/w/index.php?curid=119120894,https://upload.wikimedia.org/wikipedia/commons/f/f6/Square_D_main_circuit_breaker_from_1946.jpg,CC BY-SA 4.0,https://creativecommons.org/licenses/by-sa/4.0/,Circacies,p33,0,0,0, +p78,flickr,Old service panel,https://www.flickr.com/photos/87836984@N00/385715312,https://live.staticflickr.com/148/385715312_c0f0659ad4_b.jpg,CC BY-SA 2.0,https://creativecommons.org/licenses/by-sa/2.0/,claytron,,0,0,0, diff --git a/experiments/panel-label/src/panel_eval/screen.py b/experiments/panel-label/src/panel_eval/screen.py new file mode 100644 index 000000000..91c030440 --- /dev/null +++ b/experiments/panel-label/src/panel_eval/screen.py @@ -0,0 +1,48 @@ +"""Tally screened.csv: how many openly licensed panel photos have a legible label field. + +screened.csv lists every photo examined at full resolution after screening thumbnails, with a +per-field call made by eye: manufacturer, model or series, and amperage. Duplicates (the same +file found twice, or one photo reposted) are counted once. +""" + +import csv +from collections.abc import Iterable + +from panel_eval.paths import EXPERIMENT_DIR + +SCREENED = EXPERIMENT_DIR / "screened.csv" +# The pre-set bar: "fewer than about 40 usable photos" stops the experiment. +REQUIRED = 40 + + +def tally(rows: Iterable[dict]) -> dict[str, int]: + unique = [r for r in rows if not r["duplicate_of"]] + + def has(r: dict, field: str) -> bool: + return r[field] == "1" + + return { + "examined": len(unique), + "any field legible": sum( + 1 for r in unique if has(r, "manufacturer") or has(r, "model") or has(r, "amperage") + ), + "manufacturer and (model or amperage)": sum( + 1 for r in unique if has(r, "manufacturer") and (has(r, "model") or has(r, "amperage")) + ), + "all three": sum( + 1 for r in unique if has(r, "manufacturer") and has(r, "model") and has(r, "amperage") + ), + } + + +def main() -> None: + with SCREENED.open() as handle: + counts = tally(csv.DictReader(handle)) + for name, count in counts.items(): + print(f"{name}: {count}") + verdict = "meets" if counts["any field legible"] >= REQUIRED else "is short of" + print(f"Even the most lenient count {verdict} the {REQUIRED}-photo bar.") + + +if __name__ == "__main__": + main() diff --git a/experiments/panel-label/tests/test_panel.py b/experiments/panel-label/tests/test_panel.py new file mode 100644 index 000000000..e9e2ef3e5 --- /dev/null +++ b/experiments/panel-label/tests/test_panel.py @@ -0,0 +1,52 @@ +import io + +from PIL import Image + +from panel_eval.screen import tally +from panel_eval.sources import MAX_WIDTH, REUSABLE_COMMONS, save_upright + + +def row(dup="", m="0", s="0", a="0"): + return {"duplicate_of": dup, "manufacturer": m, "model": s, "amperage": a} + + +def test_tally_counts_each_photo_once_and_nests_the_criteria(): + rows = [ + row(m="1", s="1", a="1"), + row(dup="p01", m="1", s="1", a="1"), # a second copy of the first photo + row(m="1", a="1"), + row(a="1"), + row(), + ] + assert tally(rows) == { + "examined": 4, + "any field legible": 3, + "manufacturer and (model or amperage)": 2, + "all three": 1, + } + + +def test_commons_license_filter_accepts_only_reusable_licenses(): + for ok in ("CC0", "Public domain", "PD-USGov", "CC BY 2.0", "CC BY-SA 4.0"): + assert REUSABLE_COMMONS.match(ok), ok + for bad in ("GFDL", "CC BY-NC 2.0", "CC BY-ND 4.0", "Copyrighted", ""): + assert not REUSABLE_COMMONS.match(bad), bad + + +def test_save_upright_applies_exif_rotation_and_caps_width(tmp_path): + image = Image.new("RGB", (MAX_WIDTH * 2, 100), "white") + exif = image.getexif() + exif[0x0112] = 6 # stored rotated: display needs a 90 degree turn + buffer = io.BytesIO() + image.save(buffer, format="JPEG", exif=exif) + dest = tmp_path / "out.jpg" + save_upright(buffer.getvalue(), dest) + with Image.open(dest) as saved: + # Rotated to 100 x 7680, which is already under the width cap. + assert saved.size == (100, MAX_WIDTH * 2) + + wide = io.BytesIO() + Image.new("RGB", (MAX_WIDTH * 2, 100), "white").save(wide, format="JPEG") + save_upright(wide.getvalue(), dest) + with Image.open(dest) as saved: + assert saved.size == (MAX_WIDTH, 50) From c70ebf3798f588c9b8d63fd8955b377fbe465ca7 Mon Sep 17 00:00:00 2001 From: SamGu-NRX Date: Sat, 26 Sep 2026 07:35:03 -0500 Subject: [PATCH 3/4] experiments(panel-label): correct the search counts in the README Co-Authored-By: Claude Opus 5.5 (1M context) --- experiments/panel-label/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/experiments/panel-label/README.md b/experiments/panel-label/README.md index 1ca1f03a4..296399dd4 100644 --- a/experiments/panel-label/README.md +++ b/experiments/panel-label/README.md @@ -28,12 +28,12 @@ As an anecdote and not a measured rate: on the 8 legible US photos, Vision read ## What was searched -Between 2026-09-26 06:00 and 07:30, filtered to CC0, public domain, CC BY and CC BY-SA: +On 2026-09-26, filtered to CC0, public domain, CC BY and CC BY-SA: - **Wikimedia Commons.** The file search for each of 20 queries in `src/panel_eval/sources.py` (for example "breaker panel", "load center", "Federal Pacific", "Stab-Lok", "Zinsco", plus maker names with "breaker panel"), and the `Square D` and `Square D circuit breakers` categories. There are no Commons categories for US residential panels, load centers or breaker boxes; the related categories (`Distribution boards`, `Circuit breakers`) hold mostly European DIN-rail boards and industrial gear. - **Openverse** (openly licensed images indexed from Flickr, Commons and others): the same 20 queries at up to 100 results each, then every accessible page of 11 core queries, 1,808 results in total. The anonymous API stops at 240 results per query, so the long tails of "electrical panel", "fuse box", "load center", "circuit breaker" and "service panel" were not reachable. -After de-duplication there were about 2,000 unique results. Screening titles and thumbnails left 116 candidates; after removing duplicates and non-panels, 67 were examined at full resolution. Most results are aircraft and ship panels, European consumer units, industrial switchgear, substations, car fuse boxes and diagrams. Most US residential panel photos are wide shots of wiring in which no label text is legible; Flickr copies in Openverse are mostly 1024 px. +The first pass returned 1,476 unique results and the deeper Openverse pass 1,356 more, about 2,800 in all. Screening titles and about 850 thumbnails left 78 photos to download at full resolution; removing 11 duplicates left 67. Most results are aircraft and ship panels, European consumer units, industrial switchgear, substations, car fuse boxes and diagrams. Most US residential panel photos are wide shots of wiring in which no label text is legible; Flickr copies in Openverse are mostly 1024 px. ## What would unblock it From cf6e6e1924274523acb9116c354c58b61d8a0826 Mon Sep 17 00:00:00 2001 From: SamGu-NRX Date: Sat, 26 Sep 2026 08:13:32 -0500 Subject: [PATCH 4/4] experiments(panel-label): list the photos to take on field day Co-Authored-By: Claude Opus 5.5 (1M context) --- experiments/panel-label/README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/experiments/panel-label/README.md b/experiments/panel-label/README.md index 296399dd4..63dee5a1c 100644 --- a/experiments/panel-label/README.md +++ b/experiments/panel-label/README.md @@ -35,10 +35,22 @@ On 2026-09-26, filtered to CC0, public domain, CC BY and CC BY-SA: The first pass returned 1,476 unique results and the deeper Openverse pass 1,356 more, about 2,800 in all. Screening titles and about 850 thumbnails left 78 photos to download at full resolution; removing 11 duplicates left 67. Most results are aircraft and ship panels, European consumer units, industrial switchgear, substations, car fuse boxes and diagrams. Most US residential panel photos are wide shots of wiring in which no label text is legible; Flickr copies in Openverse are mostly 1024 px. -## What would unblock it +## What would unblock it: photos to take on field day -- **Photos the team takes or collects locally.** Like the meter photos, these would stay outside git. Ten minutes per house covers a labelled deadfront, the door label and the main breaker. About 40 panels would support the pre-set criteria, and a few older homes would cover the hazard lines. -- **Sources this search did not use.** Unsplash and Pexels photos are free to use but not CC-licensed, and whether they count as "openly licensed" is the team's call. US government works are public domain, for example CPSC recall notices, but those show individual breakers rather than panel labels. Home-inspection sites have many panel photos but are not openly licensed. +The questions can run on the team's own photos, kept in `~/house-scanning-data/panel/images` and never committed, like the meter photos. The pre-set bar needs about 40 panels, each with the photos below. Ask the homeowner's permission first. + +| Photo | Framing | Answers | +|---|---|---| +| Door label | the label inside the panel door, filling the frame, straight on | manufacturer, model or catalogue number, rated amperage | +| Main breaker | the main breaker handle and its rating, from about 20 cm | main amperage | +| Deadfront | the whole cover with the door open, including any brand badge or series name | manufacturer and line (Stab-Lok, Zinsco and other hazard lines show their name here) | +| Context | the panel and its surroundings from about 1 m | lets a reader check which panel the close-ups belong to | + +- Take each close-up the way a homeowner would, handheld with the phone's own camera, not with a tripod or macro lens. Question 3 then degrades these photos under control, as the meter sweep did, so no deliberately bad shots are needed. +- Include several homes built between about 1950 and 1985. Federal Pacific Stab-Lok and Zinsco panels were installed then, and at least a few are needed to score naming them. +- Frame out, or cover before shooting, anything that identifies the home: inspection stickers with addresses, handwritten names in the circuit directory, and the meter's serial number. + +Sources this search did not use are a second option. Unsplash and Pexels photos are free to use but not CC-licensed, and whether they count as "openly licensed" is the team's call. US government works are public domain, for example CPSC recall notices, but those show individual breakers rather than panel labels. Home-inspection sites have many panel photos but are not openly licensed. ## Rerun