Skip to content

Commit b4b0323

Browse files
Merge pull request #6 from Yaroslav-Muravev/main
Merging
2 parents e340bf4 + 1ace42f commit b4b0323

100 files changed

Lines changed: 9018 additions & 893 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.git
2+
.gitignore
3+
.claude
4+
.idea
5+
projects/thesis/results
6+
projects/thesis/_test_archive
7+
__pycache__
8+
**/__pycache__
9+
*.pyc
10+
*.pdf
11+
*.rar
12+
Master_Thesis__Bavshin_.pdf
13+
moeadd.pdf

.github/workflows/discovery.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
python -m pip install --upgrade pip
2929
pip install -r requirements.txt
3030
pip install torch
31+
pip install pytest-xdist pytest-split pytest-timeout
3132
echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV
3233
3334
- name: Run discovery tests

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,8 @@ dmypy.json
130130
.pyre/
131131
#cache
132132
/cache/*.tar
133+
134+
# Thesis run outputs (regenerated by projects/thesis/run.py + aggregators)
135+
projects/thesis/results/
136+
projects/thesis/thesis_summary.json
137+
projects/thesis/thesis_ablation_summary.json

Dockerfile

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# python:3.10-slim has glibc, just enough to install the wheels.
2+
# numpy / scipy / sklearn / torch all ship manylinux wheels, so we
3+
# don't need a build toolchain at runtime.
4+
FROM python:3.10-slim
5+
6+
# libgomp1 is needed by numpy/sklearn/torch for OpenMP threading.
7+
RUN apt-get update && apt-get install -y --no-install-recommends \
8+
libgomp1 \
9+
&& rm -rf /var/lib/apt/lists/*
10+
11+
WORKDIR /work
12+
13+
# Install Python deps first so the layer caches across source edits.
14+
COPY requirements.txt /work/requirements.txt
15+
RUN pip install --no-cache-dir --upgrade pip \
16+
&& pip install --no-cache-dir -r requirements.txt
17+
18+
# Copy the repo last so source edits don't bust the dep cache.
19+
COPY . /work
20+
21+
# Default to interactive shell; the actual sweep command is supplied
22+
# by docker-compose via the per-service ``command:`` field.
23+
ENTRYPOINT ["/bin/bash", "-l"]

docker-compose.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# 8 containers, one per ablation cell. All share the same image
2+
# and bind-mount projects/thesis/results/ so reps land on the host
3+
# filesystem and the aggregator can read them without entering a
4+
# container.
5+
#
6+
# Recommended invocation:
7+
# docker compose build
8+
# docker compose up -d # runs all 8 in background
9+
# docker compose logs -f cell-new # tail one cell's output
10+
# docker compose down # stops everything
11+
#
12+
# Resource notes:
13+
# - 16 host cores -> 2 threads/container is safe.
14+
# - 32 host cores -> bump OMP/MKL/OPENBLAS_NUM_THREADS to 4 for
15+
# ~half the wall-clock.
16+
# - The runner skips finished <cell>_rep<NN>.json files on
17+
# resume, so containers can be killed and relaunched freely.
18+
19+
services:
20+
cell-legacy: &cell-base
21+
build: .
22+
image: epde-thesis:latest
23+
command: ["scripts/run_cell.sh", "legacy"]
24+
volumes:
25+
- ./projects/thesis/results:/work/projects/thesis/results
26+
environment:
27+
OMP_NUM_THREADS: "2"
28+
MKL_NUM_THREADS: "2"
29+
OPENBLAS_NUM_THREADS: "2"
30+
restart: unless-stopped
31+
32+
cell-wape:
33+
<<: *cell-base
34+
command: ["scripts/run_cell.sh", "wape"]
35+
36+
cell-instab:
37+
<<: *cell-base
38+
command: ["scripts/run_cell.sh", "instab"]
39+
40+
cell-reg:
41+
<<: *cell-base
42+
command: ["scripts/run_cell.sh", "reg"]
43+
44+
cell-wape-instab:
45+
<<: *cell-base
46+
command: ["scripts/run_cell.sh", "wape_instab"]
47+
48+
cell-wape-reg:
49+
<<: *cell-base
50+
command: ["scripts/run_cell.sh", "wape_reg"]
51+
52+
cell-instab-reg:
53+
<<: *cell-base
54+
command: ["scripts/run_cell.sh", "instab_reg"]
55+
56+
cell-new:
57+
<<: *cell-base
58+
command: ["scripts/run_cell.sh", "new"]

epde/_loop_stats.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Lightweight retry/condition-loop instrumentation.
2+
3+
Off by default. Set ``EPDE_LOOP_STATS=1`` to enable; ~30 source-level
4+
``record(...)`` call sites then accumulate per-loop stats that
5+
``report()`` formats as a table.
6+
7+
Cost when disabled: a single global-var read per ``record`` call.
8+
Cost when enabled: a dict lookup + list append per loop exit.
9+
"""
10+
from __future__ import annotations
11+
12+
import os
13+
import sys
14+
from collections import defaultdict
15+
from typing import Optional
16+
17+
_ENABLED = os.environ.get('EPDE_LOOP_STATS', '0') == '1'
18+
19+
20+
def _new_bucket():
21+
return {'entries': 0, 'iters': [], 'hit_cap': 0, 'early_exit': 0, 'caps': set()}
22+
23+
24+
_stats = defaultdict(_new_bucket)
25+
26+
27+
def enabled() -> bool:
28+
return _ENABLED
29+
30+
31+
def record(site: str, iters: int, cap: int) -> None:
32+
"""Record one loop exit.
33+
34+
``site`` is a human label like ``"EqRPS.outer"``. ``iters`` is the
35+
number of iterations actually executed. ``cap`` is the loop's
36+
maximum (use ``sys.maxsize`` for condition-driven loops with no
37+
explicit cap).
38+
"""
39+
if not _ENABLED:
40+
return
41+
b = _stats[site]
42+
b['entries'] += 1
43+
b['iters'].append(iters)
44+
b['caps'].add(cap)
45+
if iters >= cap:
46+
b['hit_cap'] += 1
47+
elif iters <= 1:
48+
b['early_exit'] += 1
49+
50+
51+
def reset() -> None:
52+
_stats.clear()
53+
54+
55+
def _stats_for(name: str) -> dict:
56+
b = _stats[name]
57+
n = b['entries']
58+
iters = b['iters']
59+
if n == 0:
60+
return {'entries': 0}
61+
iters_sorted = sorted(iters)
62+
median = iters_sorted[n // 2]
63+
return {
64+
'entries': n,
65+
'mean': sum(iters) / n,
66+
'median': median,
67+
'max': max(iters),
68+
'p95': iters_sorted[min(n - 1, int(n * 0.95))],
69+
'total_iters': sum(iters),
70+
'hit_cap_pct': 100.0 * b['hit_cap'] / n,
71+
'early_exit_pct': 100.0 * b['early_exit'] / n,
72+
'cap': max(b['caps']) if b['caps'] else 0,
73+
}
74+
75+
76+
def report(path: Optional[str] = None) -> str:
77+
"""Format all recorded loops as a table, sorted by total iterations.
78+
79+
Writes to ``path`` if given AND also returns the string.
80+
"""
81+
sites = sorted(_stats.keys(),
82+
key=lambda s: -sum(_stats[s]['iters']) if _stats[s]['iters'] else 0)
83+
lines = []
84+
header = (f"{'site':<45} {'entries':>8} {'mean':>7} {'med':>5} "
85+
f"{'p95':>5} {'max':>5} {'cap':>6} {'%cap':>6} "
86+
f"{'%early':>7} {'totIters':>10}")
87+
lines.append(header)
88+
lines.append('-' * len(header))
89+
if not _ENABLED:
90+
lines.append('(EPDE_LOOP_STATS disabled -- set EPDE_LOOP_STATS=1 to record)')
91+
for site in sites:
92+
s = _stats_for(site)
93+
if s['entries'] == 0:
94+
continue
95+
cap_str = 'inf' if s['cap'] >= sys.maxsize else str(s['cap'])
96+
lines.append(
97+
f"{site:<45} {s['entries']:>8d} {s['mean']:>7.2f} "
98+
f"{s['median']:>5d} {s['p95']:>5d} {s['max']:>5d} "
99+
f"{cap_str:>6} {s['hit_cap_pct']:>5.1f}% "
100+
f"{s['early_exit_pct']:>6.1f}% {s['total_iters']:>10d}"
101+
)
102+
text = '\n'.join(lines)
103+
if path is not None:
104+
with open(path, 'w') as f:
105+
f.write(text + '\n')
106+
return text

epde/eq_mo_objectives.py

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -60,49 +60,40 @@ def equation_complexity_by_terms(system, equation_key):
6060
return np.count_nonzero(system.vals[equation_key].weights_internal)
6161

6262

63-
def equation_complexity_by_factors(system, equation_key):
64-
'''
65-
Evaluate the complexity of the system of PDEs, evaluating a number of factors in terms for each
66-
equation. In the evaluation, we consider only terms with non-zero weights and target, while
67-
the free coefficient is not included in the final metric. Also, the real-valued factors are
68-
not considered in the result.
69-
70-
Parameters:
71-
-----------
72-
system - ``epde.structure.main_structures.SoEq`` object
73-
The system, that is to be evaluated.
74-
75-
Returns:
76-
----------
77-
discrepancy : list of integers.
78-
The values of the error metric: list entry for each of the equations.
79-
'''
80-
# eq_compl = 0
81-
82-
# for idx, term in enumerate(system.vals[equation_key].structure):
83-
# if idx < system.vals[equation_key].target_idx:
84-
# if not system.vals[equation_key].weights_final[idx] == 0:
85-
# eq_compl += len(term.structure)
86-
# elif idx > system.vals[equation_key].target_idx:
87-
# if not system.vals[equation_key].weights_final[idx-1] == 0:
88-
# eq_compl += len(term.structure)
89-
# else:
90-
# eq_compl += len(term.structure)
91-
# return eq_compl
63+
def _complexity_single_eq(system, equation_key):
64+
# Index by ``weights_internal`` (always length ``len(structure)-1``,
65+
# one entry per non-target term in structure order) rather than
66+
# ``weights_final`` (zero-filtered to ``nnz+1`` by ``LASSOSparsity``
67+
# and ``VWSRSparsity``): structure-position indexing breaks against
68+
# ``weights_final`` whenever the sparsity step zeros more than one
69+
# weight.
70+
equation = system.vals[equation_key]
9271
eq_compl = 0
93-
94-
for idx, term in enumerate(system.vals[equation_key].structure):
95-
if idx < system.vals[equation_key].target_idx:
96-
if not system.vals[equation_key].weights_final[idx] == 0:
72+
for idx, term in enumerate(equation.structure):
73+
if idx < equation.target_idx:
74+
if not equation.weights_internal[idx] == 0:
9775
eq_compl += complexity_deriv(term.structure)
98-
elif idx > system.vals[equation_key].target_idx:
99-
if not system.vals[equation_key].weights_final[idx-1] == 0:
76+
elif idx > equation.target_idx:
77+
if not equation.weights_internal[idx-1] == 0:
10078
eq_compl += complexity_deriv(term.structure)
10179
else:
10280
eq_compl += complexity_deriv(term.structure)
10381
return eq_compl
10482

10583

84+
def equation_complexity_by_factors(system, equation_key=None):
85+
'''
86+
Evaluate the complexity of the system of PDEs as a number of factors in
87+
non-zero terms for each equation, excluding the free coefficient and
88+
real-valued factors. When ``equation_key`` is None, returns a per-equation
89+
tuple matching the ``system.vars_to_describe`` order; otherwise the scalar
90+
complexity for the named equation.
91+
'''
92+
if equation_key is None:
93+
return tuple(_complexity_single_eq(system, k) for k in system.vars_to_describe)
94+
return _complexity_single_eq(system, equation_key)
95+
96+
10697
def equation_terms_stability(system, equation_key = None):
10798
if equation_key:
10899
assert system.vals[equation_key].stability_calculated

0 commit comments

Comments
 (0)