Skip to content

Commit 777d6db

Browse files
committed
trig tokens: quantized-freq structural identity
Replace the trig-only `freq`-stripping branch in `Term.factors_labels` and the matching defensive code in `EqRightPartSelector.simplify_equation` with a uniform `Factor.structural_label` that bucketises continuous- tolerance params (e.g. trig `freq`) via `equality_ranges`. Same quantization powers `factors_labels_without_power` for the simplify common-factor scan. `cache_label` is unchanged and continues to key the tensor cache. No behavioural change for the thesis 14 systems: every system uses a narrow `freq=(v-eps, v+eps)` interval, so all sampled freq values land in bucket 0 and produce the same structural identity as the prior freq-stripping logic.
1 parent 4cf50fe commit 777d6db

3 files changed

Lines changed: 107 additions & 88 deletions

File tree

epde/operators/common/right_part_selection.py

Lines changed: 53 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -125,69 +125,60 @@ def simplify_equation(self, objective: Equation):
125125
nonzero_terms.append(objective.structure[objective.target_idx])
126126
equation_terms = [term.factors_labels_without_power for term in nonzero_terms]
127127

128-
# If amount nonzero terms is more than one -- get their intersection
129-
if len(equation_terms) > 1:
130-
common_factors = list(frozenset.intersection(*equation_terms))
131-
if len(common_factors) > 0:
132-
for common_factor in common_factors:
133-
# Find if this intersection in the same dimension (i.e. trigonometry functions) + it's minimal order
134-
min_order = np.inf
135-
common_dim = []
136-
for term in nonzero_terms:
137-
for factor in term.structure:
138-
if len(factor.params) == 1:
139-
factor_label = (factor.cache_label[0])
128+
if len(equation_terms) <= 1:
129+
return False
130+
common_factors = list(frozenset.intersection(*equation_terms))
131+
if not common_factors:
132+
return False
133+
134+
for common_factor in common_factors:
135+
# Min power across the matching factor in every nonzero term.
136+
min_order = np.inf
137+
for term in nonzero_terms:
138+
for factor in term.structure:
139+
if factor.structural_label_without_power == common_factor:
140+
if factor.cache_label[1][0] < min_order:
141+
min_order = factor.cache_label[1][0]
142+
143+
# Reduce order of common factor in every term; drop zero-power factors.
144+
max_iter = 100
145+
for term in nonzero_terms:
146+
factors_simplified = []
147+
for factor in term.structure:
148+
if factor.structural_label_without_power == common_factor:
149+
for i, value in enumerate(factor.params_description):
150+
if factor.params_description[i]["name"] == "power":
151+
factor.params[i] -= min_order
152+
if factor.params[i] == 0:
153+
factors_simplified.append(factor)
140154
else:
141-
factor_label = (factor.cache_label[0], (factor.cache_label[1][-1]))
142-
if factor_label == common_factor:
143-
if len(factor.params) > 1:
144-
common_dim.append(factor.params[-1])
145-
if factor.cache_label[1][0] < min_order:
146-
min_order = factor.cache_label[1][0]
147-
if len(set(common_dim)) < 2:
148-
# If dimension is the same -- reduce order of terms' factor
149-
max_iter = 100
150-
for term in nonzero_terms:
151-
factors_simplified = []
152-
for factor in term.structure:
153-
if len(factor.params) == 1:
154-
factor_label = (factor.cache_label[0])
155-
else:
156-
factor_label = (factor.cache_label[0], (factor.cache_label[1][-1]))
157-
if factor_label == common_factor:
158-
for i, value in enumerate(factor.params_description):
159-
if factor.params_description[i]["name"] == "power":
160-
factor.params[i] -= min_order
161-
if factor.params[i] == 0:
162-
factors_simplified.append(factor)
163-
else:
164-
continue
165-
term.structure = [factor for factor in term.structure if factor not in factors_simplified]
166-
term.reset_saved_state()
167-
168-
# If term's order became zero -- replace term.
169-
# Cap retries so a constrained token pool can't
170-
# deadlock the optimizer (same hazard fixed in
171-
# ``enforce_rps_uniqueness``).
172-
attempts = 0
173-
while attempts < max_iter:
174-
empty = len(term.structure) == 0
175-
not_meaningful = not term.contains_meaningful()
176-
signatures = {t.factors_labels for t in objective.structure}
177-
duplicate = len(signatures) != len(objective.structure)
178-
if not (empty or not_meaningful or duplicate):
179-
break
180-
term.randomize()
181-
attempts += 1
182-
183-
# Structure changed: invalidate stale fitness /
184-
# weights / AIC caches while leaving RPS to the
185-
# caller's outer loop.
186-
try:
187-
objective.reset_state(reset_right_part=False)
188-
except TypeError:
189-
objective.reset_state()
190-
return True
155+
continue
156+
term.structure = [factor for factor in term.structure if factor not in factors_simplified]
157+
term.reset_saved_state()
158+
159+
# If term's order became zero -- replace term.
160+
# Cap retries so a constrained token pool can't
161+
# deadlock the optimizer (same hazard fixed in
162+
# ``enforce_rps_uniqueness``).
163+
attempts = 0
164+
while attempts < max_iter:
165+
empty = len(term.structure) == 0
166+
not_meaningful = not term.contains_meaningful()
167+
signatures = {t.factors_labels for t in objective.structure}
168+
duplicate = len(signatures) != len(objective.structure)
169+
if not (empty or not_meaningful or duplicate):
170+
break
171+
term.randomize()
172+
attempts += 1
173+
174+
# Structure changed: invalidate stale fitness /
175+
# weights / AIC caches while leaving RPS to the
176+
# caller's outer loop.
177+
try:
178+
objective.reset_state(reset_right_part=False)
179+
except TypeError:
180+
objective.reset_state()
181+
return True
191182
return False
192183

193184
def use_default_tags(self):

epde/structure/factor.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,48 @@ def cache_label(self):
273273
cache_label = factor_params_to_str(self)
274274
return cache_label
275275

276+
def _quantized_params(self, drop_power: bool = False) -> tuple:
277+
"""Return params with continuous-tolerance ones quantized into bucket
278+
indices and exact-equality ones passed through. Continuous params
279+
(those with ``equality_ranges[name] > 0``, e.g. trig ``freq``) get
280+
``int((v - bounds[0]) / equality_ranges[name])``; exact-equality
281+
params (``power``, ``dim``) stay numeric. When ``drop_power=True``
282+
the param named ``'power'`` is omitted from the result tuple.
283+
"""
284+
parts = []
285+
for i in range(len(self.params)):
286+
name = self.params_description[i]['name']
287+
if drop_power and name == 'power':
288+
continue
289+
v = self.params[i]
290+
tol = self.equality_ranges.get(name, 0)
291+
if tol > 0:
292+
origin = self.params_description[i]['bounds'][0]
293+
parts.append(int((v - origin) / tol))
294+
else:
295+
parts.append(v)
296+
return tuple(parts)
297+
298+
@property
299+
def structural_label(self):
300+
"""Hashable canonical identity for structural dedup.
301+
302+
Sits next to ``cache_label`` (which keys the tensor cache and
303+
must stay exact). Continuous params are quantized into bucket
304+
indices so set-based dedup and ``Factor.__eq__``'s tolerance
305+
comparison agree.
306+
"""
307+
return (self.cache_label[0], self._quantized_params(drop_power=False))
308+
309+
@property
310+
def structural_label_without_power(self):
311+
"""``structural_label`` with the ``power`` param dropped.
312+
313+
Used by ``simplify_equation`` to find shared factors across
314+
terms regardless of their individual powers.
315+
"""
316+
return (self.cache_label[0], self._quantized_params(drop_power=True))
317+
276318
@property
277319
def name(self):
278320
form = self.label + '{'

epde/structure/main_structures.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -399,39 +399,25 @@ def __deepcopy__(self, memo=None):
399399

400400
@property
401401
def factors_labels_without_power(self) -> frozenset:
402-
"""Return a frozenset of factor labels with the power parameter dropped.
402+
"""Return a frozenset of structural labels with the ``power`` param dropped.
403403
404-
Each entry is either the cache label tuple or its head plus the trailing
405-
param (when the factor has more than one parameter). Used to compare
406-
terms for structural identity ignoring power differences.
404+
Identity is delegated to ``Factor.structural_label_without_power``,
405+
which quantizes continuous-tolerance params (e.g. trig ``freq``)
406+
into bucket indices so structural dedup stays consistent with
407+
``Factor.__eq__``.
407408
"""
408-
described = set()
409-
for factor in self.structure:
410-
if len(factor.params) == 1:
411-
factor_label = (factor.cache_label[0])
412-
else:
413-
factor_label = (factor.cache_label[0], (factor.cache_label[1][-1]))
414-
described.add(factor_label)
415-
described = frozenset(described)
416-
return described
409+
return frozenset(factor.structural_label_without_power for factor in self.structure)
417410

418411
@property
419412
def factors_labels(self) -> frozenset:
420-
"""Return a frozenset of canonical labels for each factor in the term.
413+
"""Return a frozenset of structural labels for each factor in the term.
421414
422-
Trigonometric factors collapse the ``freq`` parameter (kept fungible
423-
across small frequency ranges); other factors use ``factor.cache_label``
424-
directly. Used as a hashable identity for set/membership checks.
415+
Identity is delegated to ``Factor.structural_label``, which
416+
bucketises continuous-tolerance params (e.g. trig ``freq``) so
417+
within-bucket differences don't fracture structural identity.
418+
Used as a hashable identity for set/membership checks.
425419
"""
426-
described = set()
427-
for factor in self.structure:
428-
if factor.ftype == 'trigonometric':
429-
label = (factor.cache_label[0], tuple(factor.cache_label[1][i] for i, param in factor.params_description.items() if param['name'] != 'freq'))
430-
described.add(label)
431-
else:
432-
described.add(factor.cache_label)
433-
described = frozenset(described)
434-
return described
420+
return frozenset(factor.structural_label for factor in self.structure)
435421

436422
@property
437423
def term_label_without_power(self):

0 commit comments

Comments
 (0)