完善系统仿真优化计划交互
This commit is contained in:
1 parent
42ffdfff7d
commit
50c9f59d78
4 files changed
+812
-24
No files matched your search
@@ -55,6 +55,12 @@ MAX_DESIGN_VARIABLES = 16
|
||||
MAX_RESPONSE_CONSTRAINTS = 16
|
||||
MAX_SIMULATION_RUNS = 200
|
||||
MAX_WALL_SECONDS = 7 * 24 * 60 * 60
|
||||
RECOMMENDED_ALGORITHM_SEED = 0
|
||||
RECOMMENDED_POPULATION_SIZE = 8
|
||||
RECOMMENDED_MUTATION_FACTOR = 0.8
|
||||
RECOMMENDED_CROSSOVER_PROBABILITY = 0.7
|
||||
RECOMMENDED_VALIDATION_RELATIVE_TOLERANCE = 1e-8
|
||||
RECOMMENDED_VALIDATION_ABSOLUTE_TOLERANCE = 0.0
|
||||
OPTIMIZATION_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
|
||||
IDENTIFIER_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9._-]{0,63}$")
|
||||
SUPPORTED_STATISTICS = {
|
||||
@@ -306,6 +312,148 @@ class RuntimePlan:
|
||||
timeout: float
|
||||
confirmation_token: str
|
||||
|
||||
def presentation_dict(self) -> dict[str, object]:
|
||||
"""Return the strict allowlist used for an ordinary user-facing plan."""
|
||||
objective = self.spec.objective
|
||||
objective_metadata = self.variables[objective.result_key]
|
||||
payload: dict[str, object] = {
|
||||
"objective": {
|
||||
"resultKey": objective.result_key,
|
||||
"componentId": objective_metadata.get("componentId"),
|
||||
"label": objective_metadata.get("label"),
|
||||
"quantity": objective_metadata.get("quantity"),
|
||||
"statistic": objective.statistic.as_dict(),
|
||||
"goal": objective.goal.as_dict(),
|
||||
"seriesUnit": objective.expected_unit,
|
||||
"metricUnit": _metric_unit(
|
||||
objective.expected_unit,
|
||||
objective.statistic.kind,
|
||||
),
|
||||
},
|
||||
"designVariables": [
|
||||
{
|
||||
"id": item.spec.id,
|
||||
"componentId": item.spec.component_id,
|
||||
"parameter": item.spec.parameter,
|
||||
"label": item.label,
|
||||
"quantity": item.quantity,
|
||||
"current": item.initial,
|
||||
"lower": item.spec.lower,
|
||||
"upper": item.spec.upper,
|
||||
"unit": item.spec.unit,
|
||||
}
|
||||
for item in self.resolved_design_variables
|
||||
],
|
||||
"constraints": [
|
||||
{
|
||||
"id": item.id,
|
||||
"resultKey": item.result_key,
|
||||
"componentId": self.variables[item.result_key].get(
|
||||
"componentId"
|
||||
),
|
||||
"label": self.variables[item.result_key].get("label"),
|
||||
"quantity": self.variables[item.result_key].get("quantity"),
|
||||
"statistic": item.statistic.as_dict(),
|
||||
"lower": item.lower,
|
||||
"upper": item.upper,
|
||||
"tolerance": item.tolerance,
|
||||
"seriesUnit": item.expected_unit,
|
||||
"metricUnit": _metric_unit(
|
||||
item.expected_unit,
|
||||
item.statistic.kind,
|
||||
),
|
||||
}
|
||||
for item in self.spec.constraints
|
||||
],
|
||||
"runLimits": {
|
||||
"maxSimulationBudgetSlots": (
|
||||
self.spec.budget.max_simulation_runs
|
||||
),
|
||||
"searchLaunchTimeLimitSeconds": (
|
||||
self.spec.budget.max_wall_seconds
|
||||
),
|
||||
"inFlightSimulationMayFinishAfterLimit": True,
|
||||
"freshVerificationSlotsReserved": 1,
|
||||
"verificationRunsOnlyIfFeasibleCandidateFound": True,
|
||||
},
|
||||
"assumptions": {
|
||||
"continuousLinearSiDesignVariableIds": [
|
||||
item.spec.id for item in self.resolved_design_variables
|
||||
],
|
||||
"portsTopologyModesUnchanged": True,
|
||||
},
|
||||
"outputDirectory": str(self.output_directory),
|
||||
"sourceWillBeOverwritten": False,
|
||||
}
|
||||
non_default_settings: dict[str, object] = {}
|
||||
search_settings: dict[str, object] = {}
|
||||
algorithm = self.spec.algorithm
|
||||
if algorithm.seed != RECOMMENDED_ALGORITHM_SEED:
|
||||
search_settings["randomSeed"] = algorithm.seed
|
||||
if algorithm.population_size != RECOMMENDED_POPULATION_SIZE:
|
||||
search_settings["populationSize"] = algorithm.population_size
|
||||
if algorithm.mutation_factor != RECOMMENDED_MUTATION_FACTOR:
|
||||
search_settings["mutationFactor"] = algorithm.mutation_factor
|
||||
if (
|
||||
algorithm.crossover_probability
|
||||
!= RECOMMENDED_CROSSOVER_PROBABILITY
|
||||
):
|
||||
search_settings["crossoverProbability"] = (
|
||||
algorithm.crossover_probability
|
||||
)
|
||||
if search_settings:
|
||||
non_default_settings["search"] = search_settings
|
||||
|
||||
verification_settings: dict[str, object] = {}
|
||||
validation = self.spec.validation
|
||||
if (
|
||||
validation.relative_tolerance
|
||||
!= RECOMMENDED_VALIDATION_RELATIVE_TOLERANCE
|
||||
):
|
||||
verification_settings["relativeTolerance"] = (
|
||||
validation.relative_tolerance
|
||||
)
|
||||
if (
|
||||
validation.absolute_tolerance
|
||||
!= RECOMMENDED_VALIDATION_ABSOLUTE_TOLERANCE
|
||||
):
|
||||
verification_settings["absoluteTolerance"] = (
|
||||
validation.absolute_tolerance
|
||||
)
|
||||
if verification_settings:
|
||||
non_default_settings["verification"] = verification_settings
|
||||
|
||||
constraint_scales = []
|
||||
for constraint in self.spec.constraints:
|
||||
bounds = [
|
||||
abs(bound)
|
||||
for bound in (constraint.lower, constraint.upper)
|
||||
if bound is not None
|
||||
]
|
||||
recommended_scale = max(bounds, default=0.0) or 1.0
|
||||
if constraint.scale != recommended_scale:
|
||||
constraint_scales.append(
|
||||
{"id": constraint.id, "scale": constraint.scale}
|
||||
)
|
||||
if constraint_scales:
|
||||
non_default_settings["constraintRankingScales"] = (
|
||||
constraint_scales
|
||||
)
|
||||
if non_default_settings:
|
||||
payload["nonDefaultSettings"] = non_default_settings
|
||||
|
||||
search_run_limit = self.spec.budget.max_simulation_runs - 1
|
||||
population_size = self.spec.algorithm.population_size
|
||||
if (search_run_limit - population_size) // population_size < 1:
|
||||
payload["attention"] = [
|
||||
(
|
||||
"The budget can cover the initial candidate set and reserve "
|
||||
"fresh verification, but it cannot cover one complete search "
|
||||
"update cycle if every candidate is unique."
|
||||
)
|
||||
]
|
||||
return payload
|
||||
|
||||
def public_dict(self) -> dict[str, object]:
|
||||
search_run_limit = self.spec.budget.max_simulation_runs - 1
|
||||
population_size = self.spec.algorithm.population_size
|
||||
@@ -849,6 +997,86 @@ def _validate_output_target(path_text: str) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def _validate_plan_file_target(
|
||||
path_text: str,
|
||||
output_directory_text: str,
|
||||
) -> Path:
|
||||
requested_path = Path(path_text).expanduser()
|
||||
if requested_path.exists() or requested_path.is_symlink():
|
||||
raise simulation.InputError(
|
||||
"OPTIMIZATION_PLAN_FILE_ALREADY_EXISTS",
|
||||
"The saved optimization plan path must be new.",
|
||||
{"path": str(requested_path.absolute())},
|
||||
)
|
||||
try:
|
||||
path = requested_path.resolve(strict=False)
|
||||
output_directory = Path(output_directory_text).expanduser().resolve(
|
||||
strict=False
|
||||
)
|
||||
except (OSError, RuntimeError) as exc:
|
||||
raise simulation.InputError(
|
||||
"OPTIMIZATION_PLAN_FILE_PATH_INVALID",
|
||||
"The saved plan file path could not be resolved safely.",
|
||||
{"path": str(requested_path.absolute())},
|
||||
) from exc
|
||||
if path.exists():
|
||||
raise simulation.InputError(
|
||||
"OPTIMIZATION_PLAN_FILE_ALREADY_EXISTS",
|
||||
"The saved optimization plan path must be new.",
|
||||
{"path": str(path)},
|
||||
)
|
||||
if (
|
||||
path == output_directory
|
||||
or path.is_relative_to(output_directory)
|
||||
or output_directory.is_relative_to(path)
|
||||
):
|
||||
raise simulation.InputError(
|
||||
"OPTIMIZATION_PLAN_FILE_OUTPUT_CONFLICT",
|
||||
"The saved plan file and optimization output directory must be separate sibling paths.",
|
||||
{
|
||||
"planFile": str(path),
|
||||
"outputDirectory": str(output_directory),
|
||||
},
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
def _write_new_private_file(path: Path, data: bytes) -> None:
|
||||
"""Exclusively create a plan receipt readable only by its owner."""
|
||||
created = False
|
||||
descriptor: int | None = None
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
||||
if hasattr(os, "O_BINARY"):
|
||||
flags |= os.O_BINARY
|
||||
if hasattr(os, "O_NOFOLLOW"):
|
||||
flags |= os.O_NOFOLLOW
|
||||
descriptor = os.open(path, flags, 0o600)
|
||||
created = True
|
||||
if hasattr(os, "fchmod"):
|
||||
os.fchmod(descriptor, 0o600)
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
descriptor = None
|
||||
handle.write(data)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
except OSError as exc:
|
||||
if descriptor is not None:
|
||||
try:
|
||||
os.close(descriptor)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
if created and path.exists():
|
||||
path.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
raise simulation.ArtifactError(
|
||||
"OUTPUT_WRITE_FAILED", str(exc), {"path": str(path)}
|
||||
) from exc
|
||||
|
||||
|
||||
def _parameter_contracts(
|
||||
inspection: Mapping[str, object],
|
||||
) -> dict[tuple[str, str], tuple[dict[str, object], dict[str, object] | None]]:
|
||||
@@ -1173,6 +1401,18 @@ def build_runtime_plan(
|
||||
|
||||
|
||||
def command_plan(args: argparse.Namespace) -> int:
|
||||
present = bool(getattr(args, "present", False))
|
||||
plan_file_text = getattr(args, "plan_file", None)
|
||||
if present != bool(plan_file_text):
|
||||
raise simulation.InputError(
|
||||
"OPTIMIZATION_PLAN_PRESENTATION_OPTIONS_REQUIRED",
|
||||
"--present and --plan-file must be supplied together.",
|
||||
)
|
||||
plan_file = (
|
||||
_validate_plan_file_target(plan_file_text, args.output_dir)
|
||||
if plan_file_text
|
||||
else None
|
||||
)
|
||||
plan = build_runtime_plan(
|
||||
args.input,
|
||||
args.spec,
|
||||
@@ -1180,7 +1420,30 @@ def command_plan(args: argparse.Namespace) -> int:
|
||||
base_url=args.base_url,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
simulation.emit_json(plan.public_dict())
|
||||
audit_payload = plan.public_dict()
|
||||
if plan_file is not None:
|
||||
serialized = (
|
||||
json.dumps(
|
||||
audit_payload,
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
).encode("utf-8")
|
||||
_write_new_private_file(plan_file, serialized)
|
||||
if present:
|
||||
simulation.emit_json(
|
||||
{
|
||||
"ok": True,
|
||||
"command": "optimization-plan",
|
||||
"view": "presentation",
|
||||
"confirmationRequired": True,
|
||||
"presentation": plan.presentation_dict(),
|
||||
}
|
||||
)
|
||||
else:
|
||||
simulation.emit_json(audit_payload)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -3789,6 +4052,20 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
plan_parser.add_argument("input")
|
||||
plan_parser.add_argument("--spec", required=True)
|
||||
plan_parser.add_argument("--output-dir", required=True)
|
||||
plan_parser.add_argument(
|
||||
"--plan-file",
|
||||
help=(
|
||||
"Write the complete auditable plan to this new path outside the "
|
||||
"optimization output directory; requires --present."
|
||||
),
|
||||
)
|
||||
plan_parser.add_argument(
|
||||
"--present",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Emit only the user-facing plan allowlist; requires --plan-file."
|
||||
),
|
||||
)
|
||||
plan_parser.set_defaults(handler=command_plan)
|
||||
|
||||
optimize_parser = subparsers.add_parser(
|
||||
|
||||
Reference in new issue
Block a user