app
AI Flow: Reverse Engineering
Public Made by Adomby adom
Photos of a PCB to a tagged KiCad copy: parts placed, copper and nets with their evidence, a schematic, a measured confidence score, bench tests and a video of the run.
main
Caleb
Publish 0.5.0
1984b40
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
//! Two overall figures instead of one that is always 0.
//!
//! Every category's score is still the lowest of its ceilings, and every ceiling is still computed from measurements and
//! independent audits (`confidence.rs`). What changes is the grouping. Each ceiling is one of two kinds:
//!
//! - **photo**: photos can earn it. Part identity and placement, visible copper, vias, the joins a crop can show, the values
//! of marked parts, and how the drawn layers agree with the photos.
//! - **bench**: it needs the real board or an outside document. Hidden connectivity, the layer count, the values of unmarked
//! parts, and the net of copper that reaches no pin on the outer faces.
//!
//! The **photo score** is the lowest photo ceiling of the run, the **bench score** the lowest bench ceiling. The review loop's
//! target applies to the photo score. The bench score is reported beside it with what would raise it. No formula got easier:
//! the lower of the two is exactly the lowest category, which is what the single overall figure was. docs/method.md, "Two
//! scores", gives the membership and how the two mixed categories were taken apart.
use crate::confidence::{CatScore, Ceil, Scores, DEFAULT_TARGET};
use serde_json::Value;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Kind { Photo, Bench }
impl Kind { pub fn word(&self) -> &'static str { match self { Kind::Photo => "photo", Kind::Bench => "bench" } } }
pub const PHOTO_RULE: &str = "the lowest ceiling photos can earn: identity, values of marked parts, blocks, placement, copper, vias, the joins a crop can show, pins agreeing with their copper, the drawn layers against the photos";
pub const BENCH_RULE: &str = "the lowest ceiling that needs the real board or an outside document: values of unmarked parts, pins and connections no photo shows joined, the net of copper that reaches no pin, the layer count";
// Ceiling names. `confidence.rs` builds its ceilings with these, so a name cannot drift from its kind.
pub const AUDIT: &str = "independent audit";
pub const AUDIT_MARKED: &str = "independent audit, marked parts";
pub const AUDIT_UNMARKED: &str = "independent audit, unmarked parts";
pub const AUDIT_JOINS: &str = "independent audit, joins a crop shows";
pub const PINS_PROVEN: &str = "pins shown joined, by a crop or a bench probe";
pub const VALUES_MARKED: &str = "values tagged as seen, marked parts";
pub const VALUES_UNMARKED: &str = "values tagged as seen, unmarked parts";
pub const NET_AREA: &str = "copper with a net, by area";
pub const NET_LENGTH: &str = "copper with a net, by length";
pub const CONNECTIONS: &str = "connections the photos show or outside evidence proves";
pub const KICAD_CLOSED: &str = "connections KiCad finds closed";
/// The ceilings that need the real board or an outside document, by category. Every ceiling of `layers` is one. Anything not
/// listed is a photo ceiling, so a ceiling added later counts towards the loop's target until someone decides otherwise.
const BENCH: [(&str, &str); 6] = [
("values", AUDIT_UNMARKED), ("values", VALUES_UNMARKED),
("connectivity", PINS_PROVEN), ("connectivity", CONNECTIONS), ("connectivity", NET_AREA), ("connectivity", NET_LENGTH),
];
pub fn kind_of(cat: &str, ceiling: &str) -> Kind {
if cat == "layers" || (cat == "connectivity" && ceiling == KICAD_CLOSED) || BENCH.contains(&(cat, ceiling)) { Kind::Bench } else { Kind::Photo }
}
/// Which stratum a part's value belongs to: `marked` when the part carries a marking that was read, `unmarked` when it does not.
/// A photo can show a marking and nothing else of a value.
pub fn value_stratum(part: &Value) -> &'static str { if part["marking"].as_str().unwrap_or("").trim().is_empty() { "unmarked" } else { "marked" } }
/// One category's figure of one kind: the lowest ceiling of that kind, with what limits it and what would raise it.
#[derive(Clone, Debug)]
pub struct Part { pub score: f64, pub limit: String, pub raise: String, pub by_audit: bool }
fn floor1(x: f64) -> f64 { (x.max(0.0) * 10.0 + 1e-9).floor() / 10.0 }
pub fn part(cat: &str, ceilings: &[Ceil], kind: Kind) -> Option<Part> {
let low = ceilings.iter().filter(|c| kind_of(cat, &c.name) == kind).fold(None::<&Ceil>, |b, c| match b { Some(x) if x.value <= c.value => Some(x), _ => Some(c) })?;
Some(Part { score: floor1(low.value), limit: format!("{}: {}", low.name, low.inputs), raise: low.raise.clone(), by_audit: low.name.starts_with(AUDIT) })
}
/// The overall figure of one kind: the lowest of the categories' figures of that kind. With no ceiling of the kind at all, nothing was earned.
pub fn total(cats: &[CatScore], kind: Kind) -> f64 {
let v: Vec<f64> = cats.iter().filter_map(|c| match kind { Kind::Photo => c.photo.as_ref(), Kind::Bench => c.bench.as_ref() }).map(|p| p.score).collect();
if v.is_empty() { 0.0 } else { v.into_iter().fold(100.0, f64::min) }
}
pub fn show(p: &Option<Part>) -> String { p.as_ref().map(|p| format!("{:.1}", p.score)).unwrap_or_else(|| "none".into()) }
/// The line report.md must carry word for word: both figures, and which one the target applies to.
pub fn line(s: &Scores, verdict: &str) -> String {
let t = if s.target == DEFAULT_TARGET { format!("target {}", s.target) } else { format!("target {} (lowered or raised from the default {DEFAULT_TARGET})", s.target) };
format!("Confidence: photo score {:.1} of 100, {t}: {verdict}; bench score {:.1} of 100, outside the target because it needs the real board or outside documents", s.photo, s.bench)
}
/// Both tables: the photo score with the target, then the bench score with what would raise it.
pub fn tables(s: &Scores) -> String {
let mut t = String::from("Photo score: what photos can show. The review loop's target applies to it.\n\n| Category | Photo figure | Target | Limited by | Most likely to raise it |\n|---|---|---|---|---|\n");
for c in &s.cats { if let Some(p) = &c.photo { t += &format!("| {} | {:.1} | {} | {} | {} |\n", c.category, p.score, s.target, p.limit, p.raise); } }
t += &format!("| **photo score (the lowest)** | **{:.1}** | {} | | |\n", s.photo, s.target);
t += "\nBench score: what needs the real board or outside documents. Reported beside the photo score; the loop does not wait for it.\n\n| Category | Bench figure | Limited by | What would raise it |\n|---|---|---|---|\n";
for c in &s.cats { if let Some(b) = &c.bench { t += &format!("| {} | {:.1} | {} | {} |\n", c.category, b.score, b.limit, b.raise); } }
t += &format!("| **bench score (the lowest)** | **{:.1}** | | |\n", s.bench);
t
}
#[cfg(test)]
mod tests {
use super::*;
fn ceil(name: &str, value: f64) -> Ceil { Ceil { name: name.into(), value, inputs: "x".into(), raise: "y".into() } }
#[test]
fn every_layers_ceiling_is_bench_and_an_unlisted_ceiling_counts_towards_the_target() {
assert_eq!(kind_of("layers", AUDIT), Kind::Bench);
assert_eq!(kind_of("layers", "anything"), Kind::Bench);
assert_eq!(kind_of("copper", AUDIT), Kind::Photo);
assert_eq!(kind_of("copper", "a ceiling added later"), Kind::Photo);
assert_eq!(kind_of("connectivity", AUDIT_JOINS), Kind::Photo);
assert_eq!(kind_of("connectivity", "pins that agree with their copper"), Kind::Photo);
for n in [PINS_PROVEN, CONNECTIONS, NET_AREA, NET_LENGTH, KICAD_CLOSED] { assert_eq!(kind_of("connectivity", n), Kind::Bench, "{n}"); }
assert_eq!(kind_of("values", AUDIT_MARKED), Kind::Photo);
assert_eq!(kind_of("values", VALUES_MARKED), Kind::Photo);
assert_eq!(kind_of("values", AUDIT_UNMARKED), Kind::Bench);
assert_eq!(kind_of("values", VALUES_UNMARKED), Kind::Bench);
// a bench name under another category is not bench there
assert_eq!(kind_of("copper", CONNECTIONS), Kind::Photo);
}
#[test]
fn a_part_is_the_lowest_ceiling_of_its_kind_and_the_lower_part_is_the_category() {
let c = vec![ceil(AUDIT_JOINS, 46.2), ceil(PINS_PROVEN, 18.64), ceil(CONNECTIONS, 59.3), ceil("pins that agree with their copper", 100.0)];
let (p, b) = (part("connectivity", &c, Kind::Photo).unwrap(), part("connectivity", &c, Kind::Bench).unwrap());
assert_eq!(p.score, 46.2);
assert!(p.by_audit);
assert_eq!(b.score, 18.6);
assert!(!b.by_audit);
let lowest = c.iter().map(|x| x.value).fold(100.0, f64::min);
assert_eq!(p.score.min(b.score), floor1(lowest));
assert!(part("copper", &[ceil(AUDIT, 50.0)], Kind::Bench).is_none());
assert!(part("layers", &[ceil(AUDIT, 30.0)], Kind::Photo).is_none());
}
#[test]
fn a_value_is_marked_only_when_a_marking_was_read() {
assert_eq!(value_stratum(&serde_json::json!({"marking": "103"})), "marked");
assert_eq!(value_stratum(&serde_json::json!({"marking": " "})), "unmarked");
assert_eq!(value_stratum(&serde_json::json!({"value": "100nF"})), "unmarked");
}
}