Revert bad phase-field tuning pass
This commit is contained in:
parent
0808271d05
commit
ad234cd30e
4 changed files with 22 additions and 78 deletions
|
|
@ -85,6 +85,13 @@ Current hypothesis:
|
|||
- the field needs better anisotropic behavior near sharp tapered contours
|
||||
- it likely needs to distinguish motion along a stroke from motion across a stroke
|
||||
|
||||
What we just learned:
|
||||
|
||||
- the later `d9fa411` phase-field tuning pass was a real regression, not paranoia
|
||||
- that pass strengthened the local edge penalty, added a spacing loss plus upper-spacing clamp, and made edge gating more aggressive
|
||||
- on the pinned badge case, it removed internal linework and changed `2433 / 15876` output cells relative to the original good `phase-field` run
|
||||
- reverting that tuning pass restores the original good badge result exactly; the fresh fixed run under `artifacts/phase-field-regression-fix-badge-126/` is byte-identical to the original `artifacts/phase-field-v1-recheck-badge-126/`
|
||||
|
||||
Tracked fixture:
|
||||
|
||||
- `tests/fixtures/real/ai-badge-tip-focus.json`
|
||||
|
|
|
|||
|
|
@ -6,15 +6,12 @@ from dataclasses import asdict, dataclass
|
|||
@dataclass(slots=True)
|
||||
class SolverHyperParams:
|
||||
phase_field_patch_extent: float = 0.18
|
||||
phase_field_data_coherence_weight: float = 1.10
|
||||
phase_field_data_edge_weight: float = 0.55
|
||||
phase_field_data_center_edge_weight: float = 0.90
|
||||
phase_field_spacing_weight: float = 0.24
|
||||
phase_field_smoothness_weight: float = 0.26
|
||||
phase_field_edge_gate_strength: float = 11.0
|
||||
phase_field_data_coherence_weight: float = 1.00
|
||||
phase_field_data_edge_weight: float = 0.35
|
||||
phase_field_smoothness_weight: float = 0.28
|
||||
phase_field_edge_gate_strength: float = 6.0
|
||||
phase_field_collapse_weight: float = 1.20
|
||||
phase_field_min_spacing_ratio: float = 0.10
|
||||
phase_field_max_spacing_ratio: float = 1.28
|
||||
phase_field_min_spacing_ratio: float = 0.18
|
||||
phase_field_magnitude_weight: float = 0.08
|
||||
phase_field_learning_rate: float = 0.10
|
||||
phase_field_max_displacement_ratio: float = 0.48
|
||||
|
|
|
|||
|
|
@ -24,11 +24,6 @@ class _PhaseFieldPrep:
|
|||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
def _charbonnier(torch, value, epsilon: float = 1e-4):
|
||||
return torch.sqrt(value.square() + epsilon)
|
||||
|
||||
|
||||
def _require_torch():
|
||||
try:
|
||||
import torch
|
||||
|
|
@ -122,12 +117,6 @@ def _sample_patch_scalar(F, scalar_t, coords_px, offsets_t, *, width: int, heigh
|
|||
sampled = sampled[:, 0]
|
||||
return sampled.reshape(batch, out_h, out_w, samples)
|
||||
|
||||
|
||||
def _spacing_loss(torch, step, target_step: float):
|
||||
normalized = step / max(target_step, 1e-4)
|
||||
return _charbonnier(torch, normalized - 1.0).mean()
|
||||
|
||||
|
||||
def _displacement_diagnostics(uv_field: np.ndarray, selected_x, selected_y, *, width: int, height: int) -> dict[str, np.ndarray | float]:
|
||||
uv_x = (uv_field[..., 0] + 1.0) * 0.5 * max(1.0, float(width - 1))
|
||||
uv_y = (uv_field[..., 1] + 1.0) * 0.5 * max(1.0, float(height - 1))
|
||||
|
|
@ -183,8 +172,6 @@ def _project_displacements_in_place(
|
|||
*,
|
||||
min_dx: float,
|
||||
min_dy: float,
|
||||
max_step_x: float,
|
||||
max_step_y: float,
|
||||
max_dx: float,
|
||||
max_dy: float,
|
||||
width: int,
|
||||
|
|
@ -195,21 +182,13 @@ def _project_displacements_in_place(
|
|||
pos_y = (base_y_t + disp_t[..., 1]).clamp(0.0, max(0.0, float(height - 1)))
|
||||
|
||||
for index in range(1, pos_x.shape[2]):
|
||||
lower = pos_x[:, :, index - 1] + min_dx
|
||||
upper = pos_x[:, :, index - 1] + max_step_x
|
||||
pos_x[:, :, index] = torch.minimum(torch.maximum(pos_x[:, :, index], lower), upper)
|
||||
pos_x[:, :, index] = torch.maximum(pos_x[:, :, index], pos_x[:, :, index - 1] + min_dx)
|
||||
for index in range(pos_x.shape[2] - 2, -1, -1):
|
||||
lower = pos_x[:, :, index + 1] - max_step_x
|
||||
upper = pos_x[:, :, index + 1] - min_dx
|
||||
pos_x[:, :, index] = torch.maximum(torch.minimum(pos_x[:, :, index], upper), lower)
|
||||
pos_x[:, :, index] = torch.minimum(pos_x[:, :, index], pos_x[:, :, index + 1] - min_dx)
|
||||
for index in range(1, pos_y.shape[1]):
|
||||
lower = pos_y[:, index - 1, :] + min_dy
|
||||
upper = pos_y[:, index - 1, :] + max_step_y
|
||||
pos_y[:, index, :] = torch.minimum(torch.maximum(pos_y[:, index, :], lower), upper)
|
||||
pos_y[:, index, :] = torch.maximum(pos_y[:, index, :], pos_y[:, index - 1, :] + min_dy)
|
||||
for index in range(pos_y.shape[1] - 2, -1, -1):
|
||||
lower = pos_y[:, index + 1, :] - max_step_y
|
||||
upper = pos_y[:, index + 1, :] - min_dy
|
||||
pos_y[:, index, :] = torch.maximum(torch.minimum(pos_y[:, index, :], upper), lower)
|
||||
pos_y[:, index, :] = torch.minimum(pos_y[:, index, :], pos_y[:, index + 1, :] - min_dy)
|
||||
|
||||
pos_x = pos_x.clamp(0.0, max(0.0, float(width - 1)))
|
||||
pos_y = pos_y.clamp(0.0, max(0.0, float(height - 1)))
|
||||
|
|
@ -306,16 +285,8 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
|
||||
center_rgba = patch_rgba[..., 0, :]
|
||||
neighbor_rgba = patch_rgba[..., 1:, :]
|
||||
neighbor_weights = patch_rgba.new_tensor([1.0, 1.0, 1.0, 1.0, 0.75, 0.75, 0.75, 0.75]).view(1, 1, 1, -1)
|
||||
color_delta = (neighbor_rgba - center_rgba[..., None, :]).abs().mean(dim=-1)
|
||||
local_coherence = (color_delta * neighbor_weights).sum() / neighbor_weights.sum().clamp_min(1e-6) / max(
|
||||
1.0, float(color_delta.shape[0] * color_delta.shape[1] * color_delta.shape[2])
|
||||
)
|
||||
center_edge = patch_edge[..., 0].mean()
|
||||
ring_edge = ((patch_edge[..., 1:] * neighbor_weights).sum() / neighbor_weights.sum().clamp_min(1e-6)) / max(
|
||||
1.0, float(patch_edge.shape[0] * patch_edge.shape[1] * patch_edge.shape[2])
|
||||
)
|
||||
local_edge = center_edge * solver_params.phase_field_data_center_edge_weight + ring_edge
|
||||
local_coherence = (neighbor_rgba - center_rgba[..., None, :]).abs().mean()
|
||||
local_edge = patch_edge.mean()
|
||||
|
||||
disp_norm = disp_t.clone()
|
||||
disp_norm[..., 0] = disp_norm[..., 0] / max(prep.cell_x, 1e-4)
|
||||
|
|
@ -326,27 +297,24 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
smoothness = sampled_rgba.new_tensor(0.0)
|
||||
if pos_mid_x is not None:
|
||||
edge_x = _sample_scalar(F, prep.edge_t, pos_mid_x, width=prep.width, height=prep.height)
|
||||
weight_x = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_x.clamp_min(0.0))
|
||||
weight_x = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_x)
|
||||
delta_x = disp_norm[:, :, 1:, :] - disp_norm[:, :, :-1, :]
|
||||
smoothness = smoothness + (weight_x * _charbonnier(torch, delta_x, epsilon=1e-4).mean(dim=-1)).mean()
|
||||
smoothness = smoothness + (weight_x * torch.sqrt(delta_x.square().sum(dim=-1) + 1e-6)).mean()
|
||||
if pos_mid_y is not None:
|
||||
edge_y = _sample_scalar(F, prep.edge_t, pos_mid_y, width=prep.width, height=prep.height)
|
||||
weight_y = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_y.clamp_min(0.0))
|
||||
weight_y = torch.exp(-solver_params.phase_field_edge_gate_strength * edge_y)
|
||||
delta_y = disp_norm[:, 1:, :, :] - disp_norm[:, :-1, :, :]
|
||||
smoothness = smoothness + (weight_y * _charbonnier(torch, delta_y, epsilon=1e-4).mean(dim=-1)).mean()
|
||||
smoothness = smoothness + (weight_y * torch.sqrt(delta_y.square().sum(dim=-1) + 1e-6)).mean()
|
||||
|
||||
collapse = sampled_rgba.new_tensor(0.0)
|
||||
spacing = sampled_rgba.new_tensor(0.0)
|
||||
min_dx = solver_params.phase_field_min_spacing_ratio * prep.cell_x
|
||||
min_dy = solver_params.phase_field_min_spacing_ratio * prep.cell_y
|
||||
if pos_px.shape[2] > 1:
|
||||
step_x = pos_px[:, :, 1:, 0] - pos_px[:, :, :-1, 0]
|
||||
collapse = collapse + torch.relu(min_dx - step_x).square().mean()
|
||||
spacing = spacing + _spacing_loss(torch, step_x, prep.cell_x)
|
||||
if pos_px.shape[1] > 1:
|
||||
step_y = pos_px[:, 1:, :, 1] - pos_px[:, :-1, :, 1]
|
||||
collapse = collapse + torch.relu(min_dy - step_y).square().mean()
|
||||
spacing = spacing + _spacing_loss(torch, step_y, prep.cell_y)
|
||||
|
||||
magnitude = (
|
||||
(disp_t[..., 0] / max(prep.cell_x, 1e-4)).square()
|
||||
|
|
@ -357,7 +325,6 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
local_coherence * solver_params.phase_field_data_coherence_weight
|
||||
+ local_edge * solver_params.phase_field_data_edge_weight
|
||||
+ smoothness * solver_params.phase_field_smoothness_weight
|
||||
+ spacing * solver_params.phase_field_spacing_weight
|
||||
+ collapse * solver_params.phase_field_collapse_weight
|
||||
+ magnitude * solver_params.phase_field_magnitude_weight
|
||||
)
|
||||
|
|
@ -365,7 +332,6 @@ def _phase_field_loss(torch, F, prep: _PhaseFieldPrep, disp_t, solver_params: So
|
|||
"local_coherence": float(local_coherence.detach().cpu().item()),
|
||||
"local_edge": float(local_edge.detach().cpu().item()),
|
||||
"smoothness": float(smoothness.detach().cpu().item()),
|
||||
"spacing": float(spacing.detach().cpu().item()),
|
||||
"collapse": float(collapse.detach().cpu().item()),
|
||||
"magnitude": float(magnitude.detach().cpu().item()),
|
||||
}
|
||||
|
|
@ -413,8 +379,6 @@ def optimize_phase_field(
|
|||
final_terms: dict[str, float] = {}
|
||||
min_dx = solver_params.phase_field_min_spacing_ratio * prep.cell_x
|
||||
min_dy = solver_params.phase_field_min_spacing_ratio * prep.cell_y
|
||||
max_step_x = solver_params.phase_field_max_spacing_ratio * prep.cell_x
|
||||
max_step_y = solver_params.phase_field_max_spacing_ratio * prep.cell_y
|
||||
max_dx = solver_params.phase_field_max_displacement_ratio * prep.cell_x
|
||||
max_dy = solver_params.phase_field_max_displacement_ratio * prep.cell_y
|
||||
|
||||
|
|
@ -430,8 +394,6 @@ def optimize_phase_field(
|
|||
prep.base_y_t,
|
||||
min_dx=min_dx,
|
||||
min_dy=min_dy,
|
||||
max_step_x=max_step_x,
|
||||
max_step_y=max_step_y,
|
||||
max_dx=max_dx,
|
||||
max_dy=max_dy,
|
||||
width=prep.width,
|
||||
|
|
|
|||
|
|
@ -88,20 +88,6 @@ def _mutate_params(base: SolverHyperParams, rng: np.random.Generator, scale: flo
|
|||
low=0.05,
|
||||
high=2.5,
|
||||
),
|
||||
phase_field_data_center_edge_weight=_mutate_positive(
|
||||
rng,
|
||||
base.phase_field_data_center_edge_weight,
|
||||
scale=scale,
|
||||
low=0.10,
|
||||
high=3.0,
|
||||
),
|
||||
phase_field_spacing_weight=_mutate_positive(
|
||||
rng,
|
||||
base.phase_field_spacing_weight,
|
||||
scale=scale,
|
||||
low=0.01,
|
||||
high=1.0,
|
||||
),
|
||||
phase_field_smoothness_weight=_mutate_positive(
|
||||
rng,
|
||||
base.phase_field_smoothness_weight,
|
||||
|
|
@ -131,14 +117,6 @@ def _mutate_params(base: SolverHyperParams, rng: np.random.Generator, scale: flo
|
|||
low=0.01,
|
||||
high=0.45,
|
||||
),
|
||||
phase_field_max_spacing_ratio=_mutate_linear(
|
||||
rng,
|
||||
base.phase_field_max_spacing_ratio,
|
||||
scale=scale,
|
||||
sigma=0.12,
|
||||
low=0.8,
|
||||
high=1.8,
|
||||
),
|
||||
phase_field_magnitude_weight=_mutate_positive(
|
||||
rng,
|
||||
base.phase_field_magnitude_weight,
|
||||
|
|
|
|||
Loading…
Reference in a new issue