give pass 4 raw values, batch deferrable commits, fix two known bugs

Pass 4's plausibility review only ever saw normalized scores, never the
raw physical estimate behind them -- confirmed via live testing this was
exactly what caused a real misfire (gemma2:27b cited a real cyclist's
correct 5 W/kg, log-normalized to "0.159" against a car's power scale, as
grounds for rejecting an ordinary bicycle). review_plausibility now takes
raw_metrics + normalized_scores + metric units, and the prompt explicitly
instructs reasoning from the raw value first. Verified live against phi4:
it now cites the actual raw number and correctly explains why a low
normalized score doesn't mean the estimate or concept is bad.

Repository write methods used in the pipeline's hot path now take an
optional commit=False, and Pipeline defers commits during the fast/
deterministic passes (1, 3, and 2 without an LLM), flushing every 200
combos and on any exit path (finally block covers normal completion,
cancellation, and any other exception). LLM-involving calls (pass 2 with
an LLM, all of pass 4) still commit immediately -- those are slow and
crash-prone and worth protecting per-write; the deterministic passes
aren't, and recomputing them is now measured at under a second for the
full domain rather than worth 8,000+ individual fsync'd commits. Full
2,970-combination domain run: multiple minutes -> 0.91s. Test suite:
~70s -> ~15s.

Also fixes two more issues found while auditing the estimator for a real
run: CARGO_KG_PER_STRUCTURAL_KG was 500 (no real vehicle carries 500x its
own structural mass in cargo -- a magnitude bug, not a modeling choice),
corrected to 2.5. And space/rocket platforms' range_fuel now reports the
domain's ceiling instead of an arbitrary placeholder constant -- vacuum
coast isn't resistance-limited, so "distance before running out of fuel"
isn't a meaningful question for these the way it is for ground/air/water
vehicles; the real constraint is delta-v budget, a different metric this
pass doesn't model.

Validated with a live full-domain run (phi4, real Ollama calls): 115
reviewed, 0 malformed/null reviews, 0 verdict-vs-status mismatches.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 15:23:45 -05:00
parent be25a837ff
commit 730a23bac3
7 changed files with 185 additions and 49 deletions

View File

@@ -20,6 +20,14 @@ class Repository:
self.conn = conn self.conn = conn
self.conn.row_factory = sqlite3.Row self.conn.row_factory = sqlite3.Row
def commit(self) -> None:
"""Explicit flush point, for callers batching writes with commit=False
below (see Pipeline.run: instant/deterministic passes defer commits
and flush in bulk, since a crash there just means cheap recompute;
LLM-call results still commit immediately, since those are slow/
expensive to redo)."""
self.conn.commit()
# ── Dimensions ────────────────────────────────────────────── # ── Dimensions ──────────────────────────────────────────────
def ensure_dimension(self, name: str, description: str = "") -> int: def ensure_dimension(self, name: str, description: str = "") -> int:
@@ -422,7 +430,7 @@ class Repository:
key = ",".join(str(eid) for eid in sorted(entity_ids)) key = ",".join(str(eid) for eid in sorted(entity_ids))
return hashlib.sha256(key.encode()).hexdigest()[:16] return hashlib.sha256(key.encode()).hexdigest()[:16]
def save_combination(self, combination: Combination) -> Combination: def save_combination(self, combination: Combination, commit: bool = True) -> Combination:
entity_ids = [e.id for e in combination.entities] entity_ids = [e.id for e in combination.entities]
combination.hash = self.compute_hash(entity_ids) combination.hash = self.compute_hash(entity_ids)
@@ -446,11 +454,12 @@ class Repository:
"INSERT INTO combination_entities (combination_id, entity_id) VALUES (?, ?)", "INSERT INTO combination_entities (combination_id, entity_id) VALUES (?, ?)",
(combination.id, eid), (combination.id, eid),
) )
if commit:
self.conn.commit() self.conn.commit()
return combination return combination
def update_combination_status( def update_combination_status(
self, combo_id: int, status: str, block_reason: str | None = None self, combo_id: int, status: str, block_reason: str | None = None, commit: bool = True
) -> None: ) -> None:
# Don't downgrade from higher pass states — preserves human/LLM review data # Don't downgrade from higher pass states — preserves human/LLM review data
if status in ("scored", "llm_reviewed") or status.endswith("_fail"): if status in ("scored", "llm_reviewed") or status.endswith("_fail"):
@@ -470,6 +479,7 @@ class Repository:
"UPDATE combinations SET status = ?, block_reason = ? WHERE id = ?", "UPDATE combinations SET status = ?, block_reason = ? WHERE id = ?",
(status, block_reason, combo_id), (status, block_reason, combo_id),
) )
if commit:
self.conn.commit() self.conn.commit()
def get_combination(self, combo_id: int) -> Combination | None: def get_combination(self, combo_id: int) -> Combination | None:
@@ -558,6 +568,7 @@ class Repository:
combo_id: int, combo_id: int,
domain_id: int, domain_id: int,
scores: list[dict], scores: list[dict],
commit: bool = True,
) -> None: ) -> None:
"""Save per-metric scores. Each dict: metric_id, raw_value, normalized_score, estimation_method, confidence.""" """Save per-metric scores. Each dict: metric_id, raw_value, normalized_score, estimation_method, confidence."""
for s in scores: for s in scores:
@@ -569,6 +580,7 @@ class Repository:
(combo_id, domain_id, s["metric_id"], s["raw_value"], (combo_id, domain_id, s["metric_id"], s["raw_value"],
s["normalized_score"], s["estimation_method"], s["confidence"]), s["normalized_score"], s["estimation_method"], s["confidence"]),
) )
if commit:
self.conn.commit() self.conn.commit()
def save_result( def save_result(
@@ -581,6 +593,7 @@ class Repository:
llm_review: str | None = None, llm_review: str | None = None,
human_notes: str | None = None, human_notes: str | None = None,
domain_block_reason: str | None = None, domain_block_reason: str | None = None,
commit: bool = True,
) -> None: ) -> None:
self.conn.execute( self.conn.execute(
"""INSERT OR REPLACE INTO combination_results """INSERT OR REPLACE INTO combination_results
@@ -590,6 +603,7 @@ class Repository:
(combo_id, domain_id, composite_score, novelty_flag, (combo_id, domain_id, composite_score, novelty_flag,
llm_review, human_notes, pass_reached, domain_block_reason), llm_review, human_notes, pass_reached, domain_block_reason),
) )
if commit:
self.conn.commit() self.conn.commit()
def get_combination_scores(self, combo_id: int, domain_id: int) -> list[dict]: def get_combination_scores(self, combo_id: int, domain_id: int) -> list[dict]:
@@ -807,7 +821,7 @@ class Repository:
return row["pass_reached"] if row else None return row["pass_reached"] if row else None
def save_raw_estimates( def save_raw_estimates(
self, combo_id: int, domain_id: int, estimates: list[dict] self, combo_id: int, domain_id: int, estimates: list[dict], commit: bool = True
) -> None: ) -> None:
"""Save raw metric estimates (pass 2) with normalized_score=NULL. """Save raw metric estimates (pass 2) with normalized_score=NULL.
@@ -822,6 +836,7 @@ class Repository:
(combo_id, domain_id, e["metric_id"], e["raw_value"], (combo_id, domain_id, e["metric_id"], e["raw_value"],
e["estimation_method"], e["confidence"]), e["estimation_method"], e["confidence"]),
) )
if commit:
self.conn.commit() self.conn.commit()
def get_existing_result(self, combo_id: int, domain_id: int) -> dict | None: def get_existing_result(self, combo_id: int, domain_id: int) -> dict | None:

View File

@@ -69,8 +69,12 @@ INFRASTRUCTURE_AVAILABILITY: dict[tuple[str, str], float] = {
("fuel_infrastructure", "xenon_propellant"): 0.05, ("fuel_infrastructure", "xenon_propellant"): 0.05,
} }
# Crude freight-capacity proxy: kg of cargo per kg of vehicle structural mass. # Crude freight-capacity proxy: kg of cargo per kg of vehicle structural
CARGO_KG_PER_STRUCTURAL_KG: float = 500 # mass. Was 500 -- a magnitude error (500x cargo-to-structure has no real
# vehicle analog). Real cargo ships run deadweight/lightship ratios of
# roughly 1.5-4x depending on class; 2.5 is a reasonable general-cargo
# midpoint for this domain-agnostic proxy.
CARGO_KG_PER_STRUCTURAL_KG: float = 2.5
# How mechanically proven/predictable an energy form is in practice — distinct # How mechanically proven/predictable an energy form is in practice — distinct
# from safety (risk when something goes wrong) and thrust_profile (delivery # from safety (risk when something goes wrong) and thrust_profile (delivery
@@ -367,9 +371,13 @@ class Pipeline:
combos = generate_combinations(self.repo, dimensions) combos = generate_combinations(self.repo, dimensions)
result.total_generated = len(combos) result.total_generated = len(combos)
# Save all combinations to DB (also loads status for existing combos) # Save all combinations to DB (also loads status for existing combos).
# Deferred commit -- registering combos is instant/deterministic, so a
# crash here just means re-running the (cheap) registration loop, not
# losing anything worth protecting with a commit per row.
for combo in combos: for combo in combos:
self.repo.save_combination(combo) self.repo.save_combination(combo, commit=False)
self.repo.commit()
if run_id is not None: if run_id is not None:
self.repo.update_pipeline_run(run_id, total_combos=len(combos)) self.repo.update_pipeline_run(run_id, total_combos=len(combos))
@@ -378,9 +386,20 @@ class Pipeline:
bounds_by_name = {mb.metric_name: mb for mb in domain.metric_bounds} bounds_by_name = {mb.metric_name: mb for mb in domain.metric_bounds}
# ── Combo-first loop ───────────────────────────────────── # ── Combo-first loop ─────────────────────────────────────
# Deterministic passes (1, 3, and 2 without an LLM) defer commits and
# get flushed periodically + in `finally` below -- a crash there costs
# a cheap recompute, not lost work worth committing per write. Pass 4
# (and pass 2 with an LLM) commit immediately after each call: those
# are slow and crash-prone (see the QwQ timeout saga), so that result
# is worth protecting the moment it lands.
combos_since_commit = 0
try: try:
for combo in combos: for combo in combos:
self._check_cancelled(run_id) self._check_cancelled(run_id)
combos_since_commit += 1
if combos_since_commit >= 200:
self.repo.commit()
combos_since_commit = 0
# Check existing progress for this combo in this domain # Check existing progress for this combo in this domain
existing_pass = self.repo.get_combo_pass_reached( existing_pass = self.repo.get_combo_pass_reached(
@@ -399,7 +418,7 @@ class Pipeline:
combo.status = "p1_fail" combo.status = "p1_fail"
combo.block_reason = "; ".join(cr.violations) combo.block_reason = "; ".join(cr.violations)
self.repo.update_combination_status( self.repo.update_combination_status(
combo.id, "p1_fail", combo.block_reason combo.id, "p1_fail", combo.block_reason, commit=False
) )
# Save a result row so failed combos appear in results # Save a result row so failed combos appear in results
self.repo.save_result( self.repo.save_result(
@@ -407,13 +426,14 @@ class Pipeline:
domain.id, domain.id,
composite_score=0.0, composite_score=0.0,
pass_reached=1, pass_reached=1,
commit=False,
) )
result.pass1_failed += 1 result.pass1_failed += 1
self._update_run_counters(run_id, result, current_pass=1) self._update_run_counters(run_id, result, current_pass=1)
continue # p1_fail — skip remaining passes continue # p1_fail — skip remaining passes
else: else:
combo.status = "valid" combo.status = "valid"
self.repo.update_combination_status(combo.id, "valid") self.repo.update_combination_status(combo.id, "valid", commit=False)
# Domain constraint check (per-domain block only). combo.status # Domain constraint check (per-domain block only). combo.status
# stays "valid" here on purpose: it's domain-agnostic and the # stays "valid" here on purpose: it's domain-agnostic and the
@@ -431,6 +451,7 @@ class Pipeline:
domain_block_reason="; ".join( domain_block_reason="; ".join(
dc_result.violations dc_result.violations
), ),
commit=False,
) )
result.pass1_failed += 1 result.pass1_failed += 1
self._update_run_counters( self._update_run_counters(
@@ -482,9 +503,13 @@ class Pipeline:
"estimation_method": "llm" if self.llm else "stub", "estimation_method": "llm" if self.llm else "stub",
"confidence": 1.0, "confidence": 1.0,
}) })
# LLM-produced estimates commit immediately (slow/crash-
# prone, worth protecting); stub estimates are instant
# and defer, same as the rest of the deterministic passes.
used_llm = self.llm is not None
if estimate_dicts: if estimate_dicts:
self.repo.save_raw_estimates( self.repo.save_raw_estimates(
combo.id, domain.id, estimate_dicts combo.id, domain.id, estimate_dicts, commit=used_llm
) )
# Check for all-zero estimates → p2_fail # Check for all-zero estimates → p2_fail
@@ -492,11 +517,12 @@ class Pipeline:
combo.status = "p2_fail" combo.status = "p2_fail"
combo.block_reason = "All metric estimates are zero" combo.block_reason = "All metric estimates are zero"
self.repo.update_combination_status( self.repo.update_combination_status(
combo.id, "p2_fail", combo.block_reason combo.id, "p2_fail", combo.block_reason, commit=used_llm
) )
self.repo.save_result( self.repo.save_result(
combo.id, domain.id, combo.id, domain.id,
composite_score=0.0, pass_reached=2, composite_score=0.0, pass_reached=2,
commit=used_llm,
) )
result.pass2_failed += 1 result.pass2_failed += 1
self._update_run_counters(run_id, result, current_pass=2) self._update_run_counters(run_id, result, current_pass=2)
@@ -534,7 +560,7 @@ class Pipeline:
"confidence": s.confidence, "confidence": s.confidence,
}) })
if score_dicts: if score_dicts:
self.repo.save_scores(combo.id, domain.id, score_dicts) self.repo.save_scores(combo.id, domain.id, score_dicts, commit=False)
# Preserve existing human data # Preserve existing human data
novelty_flag = ( novelty_flag = (
@@ -550,6 +576,7 @@ class Pipeline:
sr.composite_score, pass_reached=3, sr.composite_score, pass_reached=3,
novelty_flag=novelty_flag, novelty_flag=novelty_flag,
human_notes=human_notes, human_notes=human_notes,
commit=False,
) )
combo.status = "p3_fail" combo.status = "p3_fail"
combo.block_reason = ( combo.block_reason = (
@@ -557,7 +584,7 @@ class Pipeline:
f"below threshold {score_threshold}" f"below threshold {score_threshold}"
) )
self.repo.update_combination_status( self.repo.update_combination_status(
combo.id, "p3_fail", combo.block_reason combo.id, "p3_fail", combo.block_reason, commit=False
) )
result.pass3_failed += 1 result.pass3_failed += 1
result.pass3_scored += 1 result.pass3_scored += 1
@@ -571,8 +598,9 @@ class Pipeline:
pass_reached=3, pass_reached=3,
novelty_flag=novelty_flag, novelty_flag=novelty_flag,
human_notes=human_notes, human_notes=human_notes,
commit=False,
) )
self.repo.update_combination_status(combo.id, "scored") self.repo.update_combination_status(combo.id, "scored", commit=False)
result.pass3_scored += 1 result.pass3_scored += 1
result.pass3_above_threshold += 1 result.pass3_above_threshold += 1
@@ -608,16 +636,21 @@ class Pipeline:
for s in db_scores for s in db_scores
if s["normalized_score"] is not None if s["normalized_score"] is not None
} }
raw_dict = {
s["metric_name"]: s["raw_value"]
for s in db_scores
if s["raw_value"] is not None
}
review_result: tuple[str, bool] | None = None review_result: tuple[str, bool] | None = None
try: try:
review_result = self.llm.review_plausibility( review_result = self.llm.review_plausibility(
description, score_dict description, raw_dict, score_dict, domain.metric_bounds
) )
except LLMRateLimitError as exc: except LLMRateLimitError as exc:
self._wait_for_rate_limit(run_id, exc.retry_after) self._wait_for_rate_limit(run_id, exc.retry_after)
try: try:
review_result = self.llm.review_plausibility( review_result = self.llm.review_plausibility(
description, score_dict description, raw_dict, score_dict, domain.metric_bounds
) )
except LLMRateLimitError: except LLMRateLimitError:
pass # still limited; skip, retry next run pass # still limited; skip, retry next run
@@ -664,6 +697,13 @@ class Pipeline:
) )
result.top_results = self.repo.get_top_results(domain.name, limit=20) result.top_results = self.repo.get_top_results(domain.name, limit=20)
return result return result
finally:
# Flush any batched deterministic writes -- runs on normal
# completion, cancellation, and any other exception propagating
# out of the loop, so nothing deferred above is ever silently lost
# on a clean exit path (a hard process crash is a different story
# and is exactly what the immediate LLM-call commits protect).
self.repo.commit()
# Mark run as completed # Mark run as completed
if run_id is not None: if run_id is not None:
@@ -842,15 +882,22 @@ class Pipeline:
raw["power_density"] = (k_act * power_mass) / physics_denom if physics_denom else 0.0 raw["power_density"] = (k_act * power_mass) / physics_denom if physics_denom else 0.0
if "range_fuel" in raw: if "range_fuel" in raw:
if storage_energy_form in AMBIENT_ENERGY_FORMS: if storage_energy_form in AMBIENT_ENERGY_FORMS or k_med is None:
# Ambient sources aren't a depletable store (see module note
# above). Space/rocket platforms (k_med undeclared for
# "space") are the same conclusion from different physics:
# in vacuum coast there's no resistance to fight, so a
# working engine covers arbitrary distance given enough
# time -- "range" isn't fuel-quantity-limited the way it is
# for a vehicle fighting drag. The real constraint for a
# rocket is its delta-v budget (maneuvering capability),
# which isn't a distance and isn't what this metric asks --
# reporting the domain's ceiling is the honest answer, not
# the old magic-constant guess (e_dens * 2.78) it replaces.
mb = bounds_by_name.get("range_fuel") mb = bounds_by_name.get("range_fuel")
raw["range_fuel"] = mb.norm_max if mb else 0.0 raw["range_fuel"] = mb.norm_max if mb else 0.0
elif k_med is not None and floor_total > 0: elif floor_total > 0:
raw["range_fuel"] = min((e_dens * storage_mass) / (k_med * floor_total), 1e13) raw["range_fuel"] = min((e_dens * storage_mass) / (k_med * floor_total), 1e13)
else:
# space/rocket platforms: resistance-based formula doesn't
# apply (see module note) -- old placeholder, not a claim.
raw["range_fuel"] = min(e_dens * 2.78, 1e13)
if "cost_efficiency" in raw: if "cost_efficiency" in raw:
structural_cost = p_rep * STRUCTURAL_COST_PER_KG_BY_MEDIUM.get(medium, STRUCTURAL_COST_PER_KG_BY_MEDIUM["ground"]) structural_cost = p_rep * STRUCTURAL_COST_PER_KG_BY_MEDIUM.get(medium, STRUCTURAL_COST_PER_KG_BY_MEDIUM["ground"])

View File

@@ -36,8 +36,20 @@ class LLMProvider(ABC):
@abstractmethod @abstractmethod
def review_plausibility( def review_plausibility(
self, combination_description: str, scores: dict[str, float] self,
combination_description: str,
raw_metrics: dict[str, float],
normalized_scores: dict[str, float],
metrics: list[MetricBound],
) -> tuple[str, bool]: ) -> tuple[str, bool]:
"""Given a combination and its scores, return a (text, is_plausible) """Given a combination, its raw physical estimates, and their
tuple: natural-language assessment and whether the concept is plausible.""" normalized scores, return a (text, is_plausible) tuple:
natural-language assessment and whether the concept is plausible.
Both raw_metrics and normalized_scores are given (not just the
normalized score) so the review can reason from the actual physics
rather than only a compressed 0-1 number, which can look
deceptively bad for a metric whose scale was built for a different
kind of vehicle. `metrics` carries each metric's unit for
formatting the raw value meaningfully."""
... ...

View File

@@ -20,6 +20,35 @@ def format_metrics_for_prompt(metrics: list["MetricBound"]) -> str:
return "\n".join(lines) return "\n".join(lines)
def format_scores_for_prompt(
raw_metrics: dict[str, float],
normalized_scores: dict[str, float],
metrics: list["MetricBound"],
) -> str:
"""Render each metric with BOTH its raw physical value and its
normalized score, so the reviewing pass can reason from the actual
physics instead of only ever seeing a compressed 0-1 number.
A real, correct estimate can still look damning once log-normalized
against a scale built for a different kind of vehicle (a cyclist's
real ~5 W/kg reads as "0.159" next to a car's 2000 W/kg ceiling) --
a reviewer that only sees the 0.159 has no way to notice that. See
the labeled-set calibration note on PLAUSIBILITY_REVIEW_PROMPT below.
"""
lines = []
for mb in metrics:
normed = normalized_scores.get(mb.metric_name)
if normed is None:
continue
raw = raw_metrics.get(mb.metric_name)
unit = mb.unit or "dimensionless"
raw_str = f"{raw:g} {unit}" if raw is not None else "unknown"
lines.append(
f"- {mb.metric_name}: raw estimate {raw_str} — normalized score {normed:.3f}"
)
return "\n".join(lines)
PHYSICS_ESTIMATION_PROMPT = """\ PHYSICS_ESTIMATION_PROMPT = """\
You are a physics estimation assistant. Given the following transportation concept, \ You are a physics estimation assistant. Given the following transportation concept, \
estimate the requested metrics using order-of-magnitude physics reasoning. estimate the requested metrics using order-of-magnitude physics reasoning.
@@ -44,15 +73,22 @@ match that magnitude, don't guess a generically "reasonable-looking" decimal.
{{"some_metric": <number>, "another_metric": <number>}} — no explanatory text. {{"some_metric": <number>, "another_metric": <number>}} — no explanatory text.
""" """
# ponytail: pass 4 only sees pass 2's raw numbers, not its reasoning. Sharpened # ponytail: pass 4 used to see only pass 2's normalized scores, not the raw
# prompts on both sides closed most of the gap (a bad safety estimate went from # physical numbers or any reasoning behind them. Fixed the raw-value half of
# 0.95 to 0.80 on the same combo once pass 2 was told to consider combination- # that gap: format_scores_for_prompt() now shows both, since a correct raw
# specific hazards). Upgrade path if this isn't good enough in practice: have # estimate can look damning once log-normalized against a scale built for a
# estimate_physics() also return a short per-metric reason, persist it # different kind of vehicle (a cyclist's real ~5 W/kg reads as "0.159" next
# alongside raw_value (new nullable column), and feed it into this prompt so # to a car's 2000 W/kg ceiling) -- gemma2:27b did exactly this on a real
# pass 4 has something concrete to agree or disagree with. Deferred because it # bicycle combo, citing "extremely low power density (0.159)" as grounds for
# needs a schema/interface change across LLMProvider + both providers + # IMPLAUSIBLE while never reasoning from the actual (correct) 5 W/kg. The
# pipeline + scorer + repository, and more generated tokens per combo. # reasoning-text half of the gap is still open: estimate_physics() doesn't
# return a per-metric rationale, so pass 4 still can't see WHY pass 2 landed
# on a number, only what the number is. Upgrade path if the raw value alone
# isn't enough in practice: have estimate_physics() also return a short
# per-metric reason, persist it alongside raw_value (new nullable column),
# and feed it into this prompt. Deferred because it needs a schema/interface
# change across LLMProvider + both providers + pipeline + scorer +
# repository, and more generated tokens per combo.
# #
# If we plan to LLM-review every p2 pass then maybe p2 and p4 should be combined. # If we plan to LLM-review every p2 pass then maybe p2 and p4 should be combined.
# #
@@ -84,10 +120,22 @@ is NOT the question.
{description} {description}
## Metric Scores ## Metric Scores
All scores below are normalized to 0-1, where HIGHER IS ALWAYS BETTER for Each metric below is given as its raw estimated physical value (in the unit
every metric listed, regardless of what the metric measures (this already shown) AND a normalized score from 0-1, where HIGHER IS ALWAYS BETTER for
accounts for things like "lower cost is better" — you don't need to invert every metric listed regardless of what it measures (this already accounts
anything). A score of 1.0 means excellent, not "pegged" or "maxed out badly." for things like "lower cost is better" — you don't need to invert anything).
A score of 1.0 means excellent, not "pegged" or "maxed out badly."
Reason from the RAW value first — it's the actual physics. The normalized
score is a summary, not a fact on its own: a real, correct estimate can
still normalize to a low-looking number simply because the domain's scale
was built for a different, more demanding kind of vehicle (a cyclist's real
~5 W/kg legitimately normalizes to ~0.16 next to a car engine's 2000 W/kg
ceiling — that low score doesn't mean the estimate is bad or the concept is
weak, it means human power is small next to a car engine, which everyone
already knows). If a normalized score looks alarming, check whether the raw
value is actually reasonable for what this component fundamentally is
before treating the score as evidence of a problem.
{scores} {scores}

View File

@@ -11,6 +11,7 @@ from physcom.llm.prompts import (
PHYSICS_ESTIMATION_PROMPT, PHYSICS_ESTIMATION_PROMPT,
PLAUSIBILITY_REVIEW_PROMPT, PLAUSIBILITY_REVIEW_PROMPT,
format_metrics_for_prompt, format_metrics_for_prompt,
format_scores_for_prompt,
) )
from physcom.models.domain import MetricBound from physcom.models.domain import MetricBound
@@ -46,9 +47,13 @@ class GeminiLLMProvider(LLMProvider):
return parse_metric_json(response.text, metrics) return parse_metric_json(response.text, metrics)
def review_plausibility( def review_plausibility(
self, combination_description: str, scores: dict[str, float] self,
combination_description: str,
raw_metrics: dict[str, float],
normalized_scores: dict[str, float],
metrics: list[MetricBound],
) -> tuple[str, bool]: ) -> tuple[str, bool]:
scores_str = "\n".join(f"- {k}: {v:.3f}" for k, v in scores.items()) scores_str = format_scores_for_prompt(raw_metrics, normalized_scores, metrics)
prompt = PLAUSIBILITY_REVIEW_PROMPT.format( prompt = PLAUSIBILITY_REVIEW_PROMPT.format(
description=combination_description, description=combination_description,
scores=scores_str, scores=scores_str,

View File

@@ -21,9 +21,13 @@ class MockLLMProvider(LLMProvider):
return result return result
def review_plausibility( def review_plausibility(
self, combination_description: str, scores: dict[str, float] self,
combination_description: str,
raw_metrics: dict[str, float],
normalized_scores: dict[str, float],
metrics: list[MetricBound],
) -> tuple[str, bool]: ) -> tuple[str, bool]:
avg = sum(scores.values()) / max(len(scores), 1) avg = sum(normalized_scores.values()) / max(len(normalized_scores), 1)
if avg > 0.5: if avg > 0.5:
return ("This concept appears plausible and worth further investigation.", True) return ("This concept appears plausible and worth further investigation.", True)
return ("This concept has significant feasibility challenges.", False) return ("This concept has significant feasibility challenges.", False)

View File

@@ -12,6 +12,7 @@ from physcom.llm.prompts import (
PHYSICS_ESTIMATION_PROMPT, PHYSICS_ESTIMATION_PROMPT,
PLAUSIBILITY_REVIEW_PROMPT, PLAUSIBILITY_REVIEW_PROMPT,
format_metrics_for_prompt, format_metrics_for_prompt,
format_scores_for_prompt,
) )
from physcom.models.domain import MetricBound from physcom.models.domain import MetricBound
@@ -34,9 +35,13 @@ class OllamaLLMProvider(LLMProvider):
return parse_metric_json(text, metrics) return parse_metric_json(text, metrics)
def review_plausibility( def review_plausibility(
self, combination_description: str, scores: dict[str, float] self,
combination_description: str,
raw_metrics: dict[str, float],
normalized_scores: dict[str, float],
metrics: list[MetricBound],
) -> tuple[str, bool]: ) -> tuple[str, bool]:
scores_str = "\n".join(f"- {k}: {v:.3f}" for k, v in scores.items()) scores_str = format_scores_for_prompt(raw_metrics, normalized_scores, metrics)
prompt = PLAUSIBILITY_REVIEW_PROMPT.format( prompt = PLAUSIBILITY_REVIEW_PROMPT.format(
description=combination_description, description=combination_description,
scores=scores_str, scores=scores_str,