Skip to content

Commit 59f4e04

Browse files
authored
Merge pull request #76 from Gromwud/main
Sparsity.py added
2 parents a15d853 + c4fb605 commit 59f4e04

38 files changed

Lines changed: 4345 additions & 635 deletions

epde/_loop_stats.py

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@
44
``record(...)`` call sites then accumulate per-loop stats that
55
``report()`` formats as a table.
66
7-
Cost when disabled: a single global-var read per ``record`` call.
8-
Cost when enabled: a dict lookup + list append per loop exit.
7+
A second metric class -- wall-clock timers -- is exposed via
8+
``timer(site)``. Same env-var gate, same ``report()`` output (second
9+
table). Both classes share the site namespace, so a site may appear in
10+
both tables (e.g., ``EqRPS.outer`` records iters; ``EqRPS.apply``
11+
records wall-clock).
12+
13+
Cost when disabled: a single global-var read per ``record`` / ``timer``
14+
call (the latter returns a shared no-op context-manager singleton).
15+
Cost when enabled: a dict lookup + list append per loop exit, plus a
16+
``perf_counter`` pair + accumulate per ``timer`` exit.
917
"""
1018
from __future__ import annotations
1119

1220
import os
1321
import sys
22+
import time
1423
from collections import defaultdict
1524
from typing import Optional
1625

@@ -21,7 +30,12 @@ def _new_bucket():
2130
return {'entries': 0, 'iters': [], 'hit_cap': 0, 'early_exit': 0, 'caps': set()}
2231

2332

33+
def _new_timer_bucket():
34+
return {'entries': 0, 'total_s': 0.0, 'max_s': 0.0}
35+
36+
2437
_stats = defaultdict(_new_bucket)
38+
_timers = defaultdict(_new_timer_bucket)
2539

2640

2741
def enabled() -> bool:
@@ -48,8 +62,78 @@ def record(site: str, iters: int, cap: int) -> None:
4862
b['early_exit'] += 1
4963

5064

65+
class _ActiveTimer:
66+
"""Per-call active timer; one instance per ``with timer(site):``."""
67+
__slots__ = ('_site', '_t0')
68+
69+
def __init__(self, site: str) -> None:
70+
self._site = site
71+
self._t0 = 0.0
72+
73+
def __enter__(self):
74+
self._t0 = time.perf_counter()
75+
return self
76+
77+
def __exit__(self, exc_type, exc, tb):
78+
dt = time.perf_counter() - self._t0
79+
b = _timers[self._site]
80+
b['entries'] += 1
81+
b['total_s'] += dt
82+
if dt > b['max_s']:
83+
b['max_s'] = dt
84+
return False
85+
86+
87+
class _NoopTimer:
88+
"""Singleton CM returned when ``EPDE_LOOP_STATS`` is off."""
89+
__slots__ = ()
90+
91+
def __enter__(self):
92+
return self
93+
94+
def __exit__(self, exc_type, exc, tb):
95+
return False
96+
97+
98+
_NOOP_TIMER = _NoopTimer()
99+
100+
101+
def timer(site: str):
102+
"""Return a context manager that accumulates wall-clock under ``site``.
103+
104+
Disabled fast path: returns a shared singleton no-op CM; the
105+
``with`` block then costs one __enter__/__exit__ method call and
106+
nothing else. Enabled path: allocates a small ``_ActiveTimer``
107+
per call.
108+
"""
109+
if not _ENABLED:
110+
return _NOOP_TIMER
111+
return _ActiveTimer(site)
112+
113+
114+
def timed(site: str):
115+
"""Decorator wrapping ``fn`` with ``timer(site)``.
116+
117+
Avoids re-indenting large ``apply`` bodies when adding probes. When
118+
``EPDE_LOOP_STATS`` is off, cost is one extra function call + the
119+
no-op CM enter/exit, all of which are negligible compared to the
120+
wrapped operator work.
121+
"""
122+
def deco(fn):
123+
def wrapper(*args, **kwargs):
124+
with timer(site):
125+
return fn(*args, **kwargs)
126+
wrapper.__wrapped__ = fn
127+
wrapper.__name__ = getattr(fn, '__name__', 'wrapper')
128+
wrapper.__qualname__ = getattr(fn, '__qualname__', 'wrapper')
129+
wrapper.__doc__ = fn.__doc__
130+
return wrapper
131+
return deco
132+
133+
51134
def reset() -> None:
52135
_stats.clear()
136+
_timers.clear()
53137

54138

55139
def _stats_for(name: str) -> dict:
@@ -73,8 +157,17 @@ def _stats_for(name: str) -> dict:
73157
}
74158

75159

160+
def timers_snapshot() -> dict:
161+
"""Return a copy of the timers dict for external consumers.
162+
163+
Used by ``profile_loop_stats.py`` to build the cross-system
164+
compare table without re-parsing the report text.
165+
"""
166+
return {site: dict(b) for site, b in _timers.items()}
167+
168+
76169
def report(path: Optional[str] = None) -> str:
77-
"""Format all recorded loops as a table, sorted by total iterations.
170+
"""Format all recorded loops + timers as two tables.
78171
79172
Writes to ``path`` if given AND also returns the string.
80173
"""
@@ -84,6 +177,7 @@ def report(path: Optional[str] = None) -> str:
84177
header = (f"{'site':<45} {'entries':>8} {'mean':>7} {'med':>5} "
85178
f"{'p95':>5} {'max':>5} {'cap':>6} {'%cap':>6} "
86179
f"{'%early':>7} {'totIters':>10}")
180+
lines.append('LOOPS')
87181
lines.append(header)
88182
lines.append('-' * len(header))
89183
if not _ENABLED:
@@ -99,6 +193,34 @@ def report(path: Optional[str] = None) -> str:
99193
f"{cap_str:>6} {s['hit_cap_pct']:>5.1f}% "
100194
f"{s['early_exit_pct']:>6.1f}% {s['total_iters']:>10d}"
101195
)
196+
197+
lines.append('')
198+
timer_sites = sorted(_timers.keys(),
199+
key=lambda s: -_timers[s]['total_s'])
200+
timer_header = (f"{'site':<45} {'entries':>8} {'total_s':>10} "
201+
f"{'mean_ms':>10} {'max_ms':>10} {'share%':>7}")
202+
lines.append('TIMERS')
203+
lines.append(timer_header)
204+
lines.append('-' * len(timer_header))
205+
if not _ENABLED:
206+
lines.append('(EPDE_LOOP_STATS disabled -- set EPDE_LOOP_STATS=1 to record)')
207+
if timer_sites:
208+
total_max = max(_timers[s]['total_s'] for s in timer_sites)
209+
else:
210+
total_max = 0.0
211+
for site in timer_sites:
212+
b = _timers[site]
213+
n = b['entries']
214+
if n == 0:
215+
continue
216+
mean_ms = 1000.0 * b['total_s'] / n
217+
max_ms = 1000.0 * b['max_s']
218+
share = (100.0 * b['total_s'] / total_max) if total_max > 0 else 0.0
219+
lines.append(
220+
f"{site:<45} {n:>8d} {b['total_s']:>10.2f} "
221+
f"{mean_ms:>10.2f} {max_ms:>10.2f} {share:>6.1f}%"
222+
)
223+
102224
text = '\n'.join(lines)
103225
if path is not None:
104226
with open(path, 'w') as f:

epde/globals.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,58 @@
2222
from epde.preprocessing.smoothers import NN
2323

2424

25+
# Gram-construction configuration, read by VWSRSparsity.apply,
26+
# PhysicsInformedLasso.fit, EqRightPartSelector._precompute_super_gram,
27+
# and L2LRFitness.apply. ``mode='vcoef'`` (default) uses the
28+
# varying-coefficient stability estimator (``VaryingCoefSetup``);
29+
# ``mode='axis'`` is the legacy axis-aligned sliding-window backup
30+
# (``GramSetup`` reduced by the var/mu^2 CV in
31+
# ``PhysicsInformedLasso.get_cv``).
32+
gram_mode: str = 'vcoef'
33+
34+
# Per-rep seed for additive Gaussian noise applied at ``cfg.load_data()``;
35+
# rewritten each rep so every rep sees an independent noise realization.
36+
noise_seed = None
37+
38+
# ``gram_mode='vcoef'`` varying-coefficient stability config (see
39+
# ``epde.operators.common.stability.VaryingCoefSetup``). ``vc_modes_cache`` resolves the
40+
# per-axis basis resolution once per ``(grid_shape, main_var)`` from the
41+
# Taylor microscale and reuses it for every candidate so the basis is
42+
# identical across individuals; cleared on ``set_gram_config``. ``vc_k_max``
43+
# caps modes per axis; ``vc_freq_coef`` scales the frequency ridge that
44+
# suppresses noise leakage into the non-constant energy.
45+
vc_modes_cache: dict = {}
46+
vc_k_max: int = 6
47+
vc_freq_coef: float = 1.0
48+
49+
# When True, ``VaryingCoefSetup._solve_gammas`` solves the mode block
50+
# PER-FEATURE (block-diagonal in feature index) instead of jointly: cross-
51+
# feature mode collinearity is dropped so a true constant-coefficient term's
52+
# region-variation B (=nc_deb/C) is not inflated by collinear grid-modulated
53+
# cousins (``x*u_xx``/``sin*u_xx`` sharing ``u_xx``'s mode energy, which pushed
54+
# the weak true term's L1 threshold above its signal -> the ac t0/3 collapse).
55+
# Extends the existing Frisch-Waugh constant-block decoupling to the modes.
56+
# Default True; set False for the legacy joint mode solve.
57+
vc_mode_decouple: bool = True
58+
59+
60+
def set_gram_config(mode: str = 'vcoef'):
61+
"""Override the global Gram-construction mode before ``build_search``.
62+
63+
Used by ``projects/thesis/thesis_runner.py`` / ``profile_loop_stats.py``
64+
to switch between the varying-coefficient default (``'vcoef'``) and the
65+
axis-aligned sliding-window backup (``'axis'``) via a single CLI flag.
66+
"""
67+
global gram_mode
68+
if mode not in ('axis', 'vcoef'):
69+
raise ValueError(
70+
f'gram_mode must be "axis" or "vcoef"; got {mode!r}')
71+
gram_mode = mode
72+
# Stale per-axis basis resolution from a prior CLI/config must not bleed
73+
# into a new invocation -- the source data or grid_shape may have changed.
74+
vc_modes_cache.clear()
75+
76+
2577
def init_caches(set_grids: bool = False, device = 'cpu'):
2678
"""
2779
Initialization global variables for keeping input data, values of grid and useful tensors such as evaluated terms

epde/interface/interface.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
import epde.globals as global_var
2424

25+
from epde import _loop_stats
26+
2527
from epde.optimizers.builder import StrategyBuilder
2628
from epde.optimizers.builder import OptimizationPatternDirector
2729

@@ -754,8 +756,9 @@ def saved_derivaties(self):
754756
print('Trying to get derivatives before their calculation. Call EPDESearch.create_pool() to calculate derivatives')
755757
return None
756758

759+
@_loop_stats.timed('EpdeSearch.fit')
757760
def fit(self, data: Union[np.ndarray, list, tuple] = None, equation_terms_max_number=6,
758-
equation_factors_max_number=1, variable_names=['u',], eq_sparsity_interval=(1e-4, 2.5),
761+
equation_factors_max_number=1, variable_names=['u',], eq_sparsity_interval=(1e-4, 2.5),
759762
derivs=None, max_deriv_order=1, additional_tokens = None, data_fun_pow: int = 1, deriv_fun_pow: int = 1,
760763
optimizer: Union[SimpleOptimizer, MOEADDOptimizer] = None, pool: TFPool = None,
761764
population: List[SoEq] = None, data_nn = None, ann_epochs_max = 1e5,

0 commit comments

Comments
 (0)