we do a little exporting

This commit is contained in:
2026-03-04 17:49:26 -06:00
parent 843baa15ad
commit fc5b3cd795
7 changed files with 720 additions and 58 deletions

View File

@@ -16,6 +16,23 @@ MUTEX_VALUES: dict[str, list[set[str]]] = {
"medium": [{"ground"}, {"water"}, {"air"}, {"space"}],
}
# Conditions assumed always available (don't need an explicit provides)
AMBIENT_CONDITIONS: set[tuple[str, str]] = {
("ground_surface", "true"),
("gravity", "true"),
("star_proximity", "true"),
}
# Per-category behavior for unmet requirements:
# "block" = hard violation, "warn" = conditional warning, "skip" = ignore
CATEGORY_SEVERITY: dict[str, str] = {
"energy": "block",
"infrastructure": "skip",
}
# For provides-vs-range_min: deficit > this ratio = hard block, else warning
DEFICIT_THRESHOLD: float = 0.25
@dataclass
class ConstraintResult:
@@ -29,8 +46,19 @@ class ConstraintResult:
class ConstraintResolver:
"""Checks a Combination's entities for dependency contradictions."""
def __init__(self, mutex_registry: dict[str, list[set[str]]] | None = None) -> None:
def __init__(
self,
mutex_registry=None,
ambient_conditions=None,
category_severity=None,
deficit_threshold=None,
) -> None:
self.mutex = mutex_registry or MUTEX_VALUES
self.ambient = ambient_conditions or AMBIENT_CONDITIONS
self.category_severity = category_severity or CATEGORY_SEVERITY
self.deficit_threshold = (
deficit_threshold if deficit_threshold is not None else DEFICIT_THRESHOLD
)
def resolve(self, combination: Combination) -> ConstraintResult:
result = ConstraintResult()
@@ -42,7 +70,7 @@ class ConstraintResolver:
self._check_requires_vs_excludes(all_deps, result)
self._check_mutual_exclusion(all_deps, result)
self._check_range_incompatibility(all_deps, result)
self._check_energy_density(combination, result)
self._check_provides_vs_range(combination, result)
self._check_unmet_requirements(all_deps, result)
if result.violations:
@@ -125,39 +153,39 @@ class ConstraintResolver:
f"but {max_name} limits {key} <= {max_val}"
)
def _check_energy_density(
def _check_provides_vs_range(
self, combination: Combination, result: ConstraintResult
) -> None:
"""Rule 6: If power source energy density << platform minimum → warn/block.
Uses a 25% threshold: below 25% of required → hard block (> 4x deficit).
"""
density_provided: list[tuple[str, float]] = []
density_required: list[tuple[str, float]] = []
"""Generic: provides(key, N) < range_min(key, M) → block/warn."""
provided: dict[str, list[tuple[str, float]]] = {}
required: dict[str, list[tuple[str, float]]] = {}
for entity in combination.entities:
for dep in entity.dependencies:
if dep.key == "energy_density" and dep.constraint_type == "provides":
density_provided.append((entity.name, float(dep.value)))
elif dep.key == "energy_density" and dep.constraint_type == "range_min":
density_required.append((entity.name, float(dep.value)))
try:
val = float(dep.value)
except (ValueError, TypeError):
continue
if dep.constraint_type == "provides":
provided.setdefault(dep.key, []).append((entity.name, val))
elif dep.constraint_type == "range_min":
required.setdefault(dep.key, []).append((entity.name, val))
for req_name, req_density in density_required:
if not density_provided:
continue # No stored energy source in this combo — skip check
for prov_name, prov_density in density_provided:
if prov_density < req_density * 0.25:
result.violations.append(
f"{prov_name} provides {prov_density:.0f} J/kg but "
f"{req_name} requires {req_density:.0f} J/kg "
f"(energy density deficit > 4x)"
)
elif prov_density < req_density:
result.warnings.append(
f"{prov_name} provides {prov_density:.0f} J/kg but "
f"{req_name} requires {req_density:.0f} J/kg "
f"(under-density)"
)
for key in set(provided) & set(required):
for req_name, req_val in required[key]:
for prov_name, prov_val in provided[key]:
if prov_val < req_val * self.deficit_threshold:
result.violations.append(
f"{prov_name} provides {key}={prov_val:.0f} but "
f"{req_name} requires {key}>={req_val:.0f} "
f"(deficit > {int(1 / self.deficit_threshold)}x)"
)
elif prov_val < req_val:
result.warnings.append(
f"{prov_name} provides {key}={prov_val:.0f} but "
f"{req_name} requires {key}>={req_val:.0f} "
f"(under-provision)"
)
def check_domain_constraints(
self, combination: Combination, constraints: list[DomainConstraint]
@@ -181,31 +209,22 @@ class ConstraintResolver:
def _check_unmet_requirements(
self, all_deps: list[tuple[str, Dependency]], result: ConstraintResult
) -> None:
"""Rule 5: Required condition not provided by any entity → conditional.
Energy-category requirements (e.g. energy_form) are hard blocks —
you cannot power an actuator with an incompatible energy source.
"""
"""Rule 5: Required condition not provided by any entity → conditional."""
provides = {(d.key, d.value) for _, d in all_deps if d.constraint_type == "provides"}
# Ambient conditions that don't need to be explicitly provided
ambient = {
("ground_surface", "true"),
("gravity", "true"),
("star_proximity", "true"),
}
for name, dep in all_deps:
if dep.constraint_type != "requires":
continue
if dep.category == "infrastructure":
continue # Infrastructure is external, not checked here
severity = self.category_severity.get(dep.category, "warn")
if severity == "skip":
continue
key_val = (dep.key, dep.value)
if key_val not in provides and key_val not in ambient:
if key_val not in provides and key_val not in self.ambient:
msg = (
f"{name} requires {dep.key}={dep.value} "
f"but no entity in this combination provides it"
)
if dep.category == "energy":
if severity == "block":
result.violations.append(msg)
else:
result.warnings.append(msg)