we do a little exporting
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user