Compare commits
No commits in common. "b0fe7e8ae94abf15270572ac7e51a8673087f66d" and "b28d748daac4c8dd4bd30378ff09b6d4d74ed01e" have entirely different histories.
b0fe7e8ae9
...
b28d748daa
6 changed files with 17 additions and 186 deletions
File diff suppressed because one or more lines are too long
75
DESIGN.md
75
DESIGN.md
|
|
@ -1108,81 +1108,6 @@ core as `fixed_circ`; threaded through `search_staged(seed_adjacency_aware=True)
|
||||||
denser, more-circulation layouts), which is the canonical-encoding /
|
denser, more-circulation layouts), which is the canonical-encoding /
|
||||||
shape-feasibility territory of `homemaker-py-9gp`.
|
shape-feasibility territory of `homemaker-py-9gp`.
|
||||||
|
|
||||||
### 11.8 Topology diversity × selection pressure, co-tuned (`homemaker-py-6zy`) — DONE (negative)
|
|
||||||
|
|
||||||
Premise (loose end from §11.5): structural niching was A/B'd against the legacy
|
|
||||||
fitness-scalar dedup with selection pressure **held fixed** at a binary tournament
|
|
||||||
(`driver._tournament`, `k=2`). §11.5's own mechanism note named the coupling as
|
|
||||||
the reason for its null — "Maximal diversity (16/16) with the fixed tournament
|
|
||||||
pressure just **diffuses** effort" — i.e. diversity and pressure are coupled but
|
|
||||||
were varied as if independent: niching widens the population, but `k` was never
|
|
||||||
**sharpened** to convert the extra exploration back into exploitation. This issue
|
|
||||||
isolates that coupling — sweep tournament size jointly with niching to test
|
|
||||||
whether sharper selection turns the 16/16 structural diversity into lower fails.
|
|
||||||
The project had already pivoted to the canonical encoding (`homemaker-py-9gp`);
|
|
||||||
this is a falsification check so the lever is not silently lost, not an expected
|
|
||||||
win (§11.4/§11.5 both located the plateau in **reachability**).
|
|
||||||
|
|
||||||
**Implementation (knob only; default-off behaviour unchanged).** Exposed
|
|
||||||
`tournament_k: int = 2` on `search` / `search_staged`, threaded into both
|
|
||||||
`_tournament` call sites (crossover pair + mutation parent) and all three internal
|
|
||||||
`search()` calls of the staged path; reuses the §11.5 `genome.signature` /
|
|
||||||
`niche_by_signature` machinery unchanged. The experiments harness reads
|
|
||||||
`HOMEMAKER_TOURNAMENT_K` (mirrors `NICHE`) in `run_search_scaled.py` /
|
|
||||||
`run_staged_search.py`; `experiments/run_6zy_ab.sh` runs the joint grid (RESUME-able).
|
|
||||||
|
|
||||||
- *Commands (reproduce, `URB_NO_OCCLUSION=1`, 20000 evals; blank-slate seed
|
|
||||||
`init.dom` to match §11.5):*
|
|
||||||
```bash
|
|
||||||
# grid: NICHE ∈ {0,1} × HOMEMAKER_TOURNAMENT_K ∈ {2,3,4}
|
|
||||||
NICHE=0 HOMEMAKER_TOURNAMENT_K=2 python3 experiments/run_search_scaled.py \
|
|
||||||
examples/programme-house 20000 <seed> examples/programme-house/init.dom scratch/out.dom
|
|
||||||
# harbor (staged): run_staged_search.py, seed examples/harbor-house/init.dom
|
|
||||||
bash experiments/run_6zy_ab.sh # full grid → scratch/6zy/summary.tsv
|
|
||||||
```
|
|
||||||
|
|
||||||
- *Diversity (mechanism check) — confirmed biting.* `niche=on` holds the final
|
|
||||||
population at **16/16** distinct topologies at every `k`; `niche=off` sits at
|
|
||||||
**4–11/16**. The pressure knob is genuinely varied (`k`=2,3,4). So both levers
|
|
||||||
are live — the null below is not a machinery artefact.
|
|
||||||
|
|
||||||
- *Fail count (the gate) — no cell beats the baseline.* Blank-slate
|
|
||||||
programme-house, total fails at budget over **5 seeds** (0–4), mean (sd):
|
|
||||||
|
|
||||||
| niche \ k | k=2 | k=3 | k=4 |
|
|
||||||
|:---------:|:-----------:|:-----------:|:-----------:|
|
|
||||||
| **off** | **4.80** (1.60) | 6.40 (2.50) | 6.00 (2.00) |
|
|
||||||
| **on** | 6.20 (1.72) | 7.00 (1.41) | 6.60 (1.85) |
|
|
||||||
|
|
||||||
The legacy `(off, k=2)` cell is the **best** of the six (4.80); every
|
|
||||||
higher-pressure row and every `niche=on` row is equal-or-worse (6.0–7.0). All
|
|
||||||
differences sit within ~1 sd at 5 seeds, so the grid is a wash — but the central
|
|
||||||
tendency is unambiguous: sharpening `k` and adding niching both *slightly hurt*,
|
|
||||||
the opposite of the rescue the premise hypothesised. Harbor-house (staged, seed
|
|
||||||
0) reinforces it: `niche=on` is uniformly worse than `off` at every `k`
|
|
||||||
(k2 72→83, k3 77→82, k4 67→75); within the `niche=on` row higher `k` helps
|
|
||||||
monotonically (83→82→75) but never catches the `niche=off` row, and the best
|
|
||||||
cell overall (`off, k=4` = 67) is a single-seed wiggle within noise of the
|
|
||||||
`off, k=2` = 72 baseline.
|
|
||||||
|
|
||||||
- *Why it fails — the coupling is real but points the wrong way.* Sharper
|
|
||||||
selection does **not** convert the extra structural diversity into lower fails;
|
|
||||||
if anything the 16/16 niched population at high `k` over-commits the
|
|
||||||
larger spread to a handful of basins and loses the occasional lucky low-fail
|
|
||||||
draw the smaller fitness-scalar population stumbles into. §11.5's "diffuses
|
|
||||||
effort" diagnosis survives co-tuning: the bottleneck is **reachability**
|
|
||||||
(operators + encoding cannot reach the low-fail basins), so reshaping
|
|
||||||
selection/population pressure cannot recover what the search space does not
|
|
||||||
expose — the same conclusion §11.4 reached from the comparator side and §11.5
|
|
||||||
from the diversity side.
|
|
||||||
|
|
||||||
- *Verdict: §11.5 null is robust to selection pressure — reject `k>2` and niching
|
|
||||||
as defaults; binary tournament + fitness-scalar dedup stand.* `tournament_k` is
|
|
||||||
kept (default-2) as a reusable knob alongside `niche_by_signature`. With
|
|
||||||
§11.4/§11.5/§11.8 all negative on the outer loop, the residual is confirmed
|
|
||||||
structural: the principled lever is the canonical encoding + richer topology
|
|
||||||
operators (`homemaker-py-9gp`), not selection or population management.
|
|
||||||
|
|
||||||
## 12. Phase 7 — scaling validation & residual reduction (post-c4c)
|
## 12. Phase 7 — scaling validation & residual reduction (post-c4c)
|
||||||
|
|
||||||
**Epic:** `homemaker-py-leu`. **Status:** opened 2026-06-19. Continuation of the
|
**Epic:** `homemaker-py-leu`. **Status:** opened 2026-06-19. Continuation of the
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# 6zy joint A/B: topology diversity (structural niching) x selection pressure
|
|
||||||
# (tournament size k). §11.5 raised diversity to 16/16 but held pressure FIXED at
|
|
||||||
# k=2; this sweep co-tunes them to test whether sharper selection converts the
|
|
||||||
# extra structural diversity into lower fails rather than diffusing it.
|
|
||||||
#
|
|
||||||
# Grid: NICHE in {0,1} x HOMEMAKER_TOURNAMENT_K in {2,3,4}.
|
|
||||||
# (niche=0,k=2) is the legacy baseline; (niche=1,k=2) reproduces §11.5's niche
|
|
||||||
# column; the k in {3,4} rows are the new higher-pressure cells.
|
|
||||||
# Seeds: programme-house 0..4 (>=5, widened from §11.5's thin 3 to clear seed
|
|
||||||
# noise); harbor-house staged seed 0.
|
|
||||||
# Equal native-fitness budget, URB_NO_OCCLUSION=1. Results -> scratch/6zy/summary.tsv
|
|
||||||
set -u
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
OUT=scratch/6zy
|
|
||||||
mkdir -p "$OUT"
|
|
||||||
SUMMARY="$OUT/summary.tsv"
|
|
||||||
# RESUME=1 keeps an existing summary and skips cells already recorded (non-ERR),
|
|
||||||
# so a killed sweep can finish only its missing cells. Default starts fresh.
|
|
||||||
if [ "${RESUME:-0}" = 1 ] && [ -s "$SUMMARY" ]; then
|
|
||||||
echo "RESUME: keeping $(($(wc -l < "$SUMMARY") - 1)) existing rows"
|
|
||||||
else
|
|
||||||
: > "$SUMMARY"
|
|
||||||
printf 'prog\tniche\tk\tseed\tfails\tseen\tpop_distinct\trestarts\ttag\n' >> "$SUMMARY"
|
|
||||||
fi
|
|
||||||
BUDGET=${BUDGET:-20000}
|
|
||||||
PH_SEEDS=${PH_SEEDS:-"0 1 2 3 4"}
|
|
||||||
HARBOR_SEEDS=${HARBOR_SEEDS:-"0"}
|
|
||||||
MAXJOBS=${MAXJOBS:-2}
|
|
||||||
# §11.5 reproduce-commands seed programme-house from init.dom (a bare undivided
|
|
||||||
# plot) so the run is a true BLANK-SLATE topology search; c964…dom is a finished
|
|
||||||
# design that would warm-start and floor trivially. Must match §11.5 for the
|
|
||||||
# direct comparison the acceptance criteria require.
|
|
||||||
PH_SEED_FILE=examples/programme-house/init.dom
|
|
||||||
|
|
||||||
run_one() {
|
|
||||||
local prog=$1 harness=$2 niche=$3 k=$4 seed=$5 seedfile=$6
|
|
||||||
local tag="${prog}_n${niche}_k${k}_s${seed}"
|
|
||||||
local log="$OUT/${tag}.log"
|
|
||||||
if [ "${RESUME:-0}" = 1 ] && grep -qP "\t${tag}\$" "$SUMMARY" 2>/dev/null \
|
|
||||||
&& ! grep -qP "\tERR\t.*\t${tag}\$" "$SUMMARY" 2>/dev/null; then
|
|
||||||
echo "SKIP $tag (already recorded)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
URB_NO_OCCLUSION=1 NICHE=$niche HOMEMAKER_TOURNAMENT_K=$k \
|
|
||||||
python3 "experiments/$harness" \
|
|
||||||
"examples/$prog" "$BUDGET" "$seed" "$seedfile" \
|
|
||||||
"$OUT/${tag}.dom" > "$log" 2>&1
|
|
||||||
local fails seen popd restarts
|
|
||||||
fails=$(grep -oE 'best *: [0-9.eE+-]+ \([0-9]+ fails\)' "$log" | grep -oE '[0-9]+ fails' | grep -oE '[0-9]+' | tail -1)
|
|
||||||
seen=$(grep -oE 'diversity : [0-9]+ distinct topologies seen' "$log" | grep -oE '[0-9]+' | tail -1)
|
|
||||||
popd=$(grep -oE '[0-9]+/[0-9]+ distinct in final population' "$log" | head -1 | grep -oE '^[0-9]+')
|
|
||||||
restarts=$(grep -oE '[0-9]+ restarts' "$log" | grep -oE '[0-9]+' | tail -1)
|
|
||||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
|
||||||
"$prog" "$niche" "$k" "$seed" "${fails:-ERR}" "${seen:-?}" "${popd:-?}" "${restarts:-?}" "$tag" >> "$SUMMARY"
|
|
||||||
echo "DONE $tag -> ${fails:-ERR} fails (seen=${seen:-?}, popd=${popd:-?})"
|
|
||||||
}
|
|
||||||
|
|
||||||
JOBS=()
|
|
||||||
for niche in 0 1; do
|
|
||||||
for k in 2 3 4; do
|
|
||||||
for seed in $PH_SEEDS; do
|
|
||||||
JOBS+=("programme-house|run_search_scaled.py|$niche|$k|$seed|$PH_SEED_FILE")
|
|
||||||
done
|
|
||||||
for seed in $HARBOR_SEEDS; do
|
|
||||||
JOBS+=("harbor-house|run_staged_search.py|$niche|$k|$seed|examples/harbor-house/init.dom")
|
|
||||||
done
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "queued ${#JOBS[@]} jobs, budget=$BUDGET, maxjobs=$MAXJOBS"
|
|
||||||
for job in "${JOBS[@]}"; do
|
|
||||||
IFS='|' read -r prog harness niche k seed seedfile <<< "$job"
|
|
||||||
while [ "$(jobs -rp | wc -l)" -ge "$MAXJOBS" ]; do wait -n; done
|
|
||||||
run_one "$prog" "$harness" "$niche" "$k" "$seed" "$seedfile" &
|
|
||||||
done
|
|
||||||
wait
|
|
||||||
echo "ALL DONE"
|
|
||||||
# keep header first, sort the rest
|
|
||||||
{ head -1 "$SUMMARY"; tail -n +2 "$SUMMARY" | sort; } > "$SUMMARY.tmp" && mv "$SUMMARY.tmp" "$SUMMARY"
|
|
||||||
cat "$SUMMARY"
|
|
||||||
|
|
@ -71,8 +71,6 @@ def main() -> int:
|
||||||
# fitness-scalar dedup (the "before"); RESTART_PATIENCE=<evals> enables soft
|
# fitness-scalar dedup (the "before"); RESTART_PATIENCE=<evals> enables soft
|
||||||
# restarts (default off).
|
# restarts (default off).
|
||||||
niche = os.environ.get("NICHE", "0") == "1"
|
niche = os.environ.get("NICHE", "0") == "1"
|
||||||
# 6zy: tournament size (selection pressure), default k=2 (legacy binary).
|
|
||||||
tournament_k = int(os.environ.get("HOMEMAKER_TOURNAMENT_K", "2"))
|
|
||||||
rp = os.environ.get("RESTART_PATIENCE")
|
rp = os.environ.get("RESTART_PATIENCE")
|
||||||
restart_patience = int(rp) if rp else None
|
restart_patience = int(rp) if rp else None
|
||||||
adj = os.environ.get("ADJ", "1") == "1" # s44 adjacency-aware seeding
|
adj = os.environ.get("ADJ", "1") == "1" # s44 adjacency-aware seeding
|
||||||
|
|
@ -84,7 +82,6 @@ def main() -> int:
|
||||||
print(f"rng seed : {rng_seed}")
|
print(f"rng seed : {rng_seed}")
|
||||||
print(f"use_grade : {use_grade}")
|
print(f"use_grade : {use_grade}")
|
||||||
print(f"niche : {niche}")
|
print(f"niche : {niche}")
|
||||||
print(f"tourn_k : {tournament_k}")
|
|
||||||
print(f"restart_p : {restart_patience}")
|
print(f"restart_p : {restart_patience}")
|
||||||
print(f"adj_aware : {adj}")
|
print(f"adj_aware : {adj}")
|
||||||
print(f"prop_aware: {prop}")
|
print(f"prop_aware: {prop}")
|
||||||
|
|
@ -104,7 +101,6 @@ def main() -> int:
|
||||||
seed=rng_seed,
|
seed=rng_seed,
|
||||||
log=lambda m: print(m, flush=True),
|
log=lambda m: print(m, flush=True),
|
||||||
use_grade=use_grade,
|
use_grade=use_grade,
|
||||||
tournament_k=tournament_k,
|
|
||||||
niche_by_signature=niche,
|
niche_by_signature=niche,
|
||||||
restart_patience=restart_patience,
|
restart_patience=restart_patience,
|
||||||
seed_adjacency_aware=adj,
|
seed_adjacency_aware=adj,
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,6 @@ def main() -> int:
|
||||||
|
|
||||||
use_grade = os.environ.get("USE_GRADE") == "1" # §11.4 graded objective A/B
|
use_grade = os.environ.get("USE_GRADE") == "1" # §11.4 graded objective A/B
|
||||||
niche = os.environ.get("NICHE", "0") == "1" # §11.5 structural niching A/B
|
niche = os.environ.get("NICHE", "0") == "1" # §11.5 structural niching A/B
|
||||||
tournament_k = int(os.environ.get("HOMEMAKER_TOURNAMENT_K", "2")) # 6zy pressure knob
|
|
||||||
rp = os.environ.get("RESTART_PATIENCE")
|
rp = os.environ.get("RESTART_PATIENCE")
|
||||||
restart_patience = int(rp) if rp else None
|
restart_patience = int(rp) if rp else None
|
||||||
adj = os.environ.get("ADJ", "1") == "1" # s44/ld5 adjacency-aware seeding A/B
|
adj = os.environ.get("ADJ", "1") == "1" # s44/ld5 adjacency-aware seeding A/B
|
||||||
|
|
@ -97,7 +96,6 @@ def main() -> int:
|
||||||
print(f"rng seed : {rng_seed}")
|
print(f"rng seed : {rng_seed}")
|
||||||
print(f"use_grade : {use_grade}")
|
print(f"use_grade : {use_grade}")
|
||||||
print(f"niche : {niche}")
|
print(f"niche : {niche}")
|
||||||
print(f"tourn_k : {tournament_k}")
|
|
||||||
print(f"restart_p : {restart_patience}")
|
print(f"restart_p : {restart_patience}")
|
||||||
print(f"adj_aware : {adj}")
|
print(f"adj_aware : {adj}")
|
||||||
print(f"prop_aware: {prop}")
|
print(f"prop_aware: {prop}")
|
||||||
|
|
@ -125,7 +123,6 @@ def main() -> int:
|
||||||
seed=rng_seed,
|
seed=rng_seed,
|
||||||
log=lambda m: print(m, flush=True),
|
log=lambda m: print(m, flush=True),
|
||||||
use_grade=use_grade,
|
use_grade=use_grade,
|
||||||
tournament_k=tournament_k,
|
|
||||||
niche_by_signature=niche,
|
niche_by_signature=niche,
|
||||||
restart_patience=restart_patience,
|
restart_patience=restart_patience,
|
||||||
seed_adjacency_aware=adj,
|
seed_adjacency_aware=adj,
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,6 @@ def search(
|
||||||
base_p: float = 1.0,
|
base_p: float = 1.0,
|
||||||
child_probe=None,
|
child_probe=None,
|
||||||
use_grade: bool = False,
|
use_grade: bool = False,
|
||||||
tournament_k: int = 2,
|
|
||||||
niche_by_signature: bool = False,
|
niche_by_signature: bool = False,
|
||||||
restart_patience: int | None = None,
|
restart_patience: int | None = None,
|
||||||
restart_elite: int = 1,
|
restart_elite: int = 1,
|
||||||
|
|
@ -479,14 +478,13 @@ def search(
|
||||||
tasks = []
|
tasks = []
|
||||||
for _ in range(batch_n):
|
for _ in range(batch_n):
|
||||||
if len(pop) >= 2 and rng.random() < p_crossover:
|
if len(pop) >= 2 and rng.random() < p_crossover:
|
||||||
a, b = (_tournament(pop, rng, _key, k=tournament_k),
|
a, b = _tournament(pop, rng, _key), _tournament(pop, rng, _key)
|
||||||
_tournament(pop, rng, _key, k=tournament_k))
|
|
||||||
child_root, _, desc = operators.crossover(a.root, b.root, rng)
|
child_root, _, desc = operators.crossover(a.root, b.root, rng)
|
||||||
if child_probe is not None:
|
if child_probe is not None:
|
||||||
desc = f"{desc}|pf={a.n_fails},{b.n_fails}"
|
desc = f"{desc}|pf={a.n_fails},{b.n_fails}"
|
||||||
ratios = {**b.ratios, **a.ratios} # primary parent wins
|
ratios = {**b.ratios, **a.ratios} # primary parent wins
|
||||||
else:
|
else:
|
||||||
parent = _tournament(pop, rng, _key, k=tournament_k)
|
parent = _tournament(pop, rng, _key)
|
||||||
child_root, desc = operators.mutate(parent.root, rng, types,
|
child_root, desc = operators.mutate(parent.root, rng, types,
|
||||||
weights=mutation_weights,
|
weights=mutation_weights,
|
||||||
reqs=reqs, base_p=base_p)
|
reqs=reqs, base_p=base_p)
|
||||||
|
|
@ -539,7 +537,6 @@ def search_staged(
|
||||||
log=None,
|
log=None,
|
||||||
n_workers: int = 1,
|
n_workers: int = 1,
|
||||||
use_grade: bool = False,
|
use_grade: bool = False,
|
||||||
tournament_k: int = 2,
|
|
||||||
niche_by_signature: bool = False,
|
niche_by_signature: bool = False,
|
||||||
restart_patience: int | None = None,
|
restart_patience: int | None = None,
|
||||||
restart_elite: int = 1,
|
restart_elite: int = 1,
|
||||||
|
|
@ -594,8 +591,7 @@ def search_staged(
|
||||||
child_budget=child_budget, seed_budget=seed_budget,
|
child_budget=child_budget, seed_budget=seed_budget,
|
||||||
p_crossover=p_crossover, seed=seed, types=types,
|
p_crossover=p_crossover, seed=seed, types=types,
|
||||||
inner_kw=inner_kw, log=log, n_workers=n_workers,
|
inner_kw=inner_kw, log=log, n_workers=n_workers,
|
||||||
use_grade=use_grade, tournament_k=tournament_k,
|
use_grade=use_grade, niche_by_signature=niche_by_signature,
|
||||||
niche_by_signature=niche_by_signature,
|
|
||||||
restart_patience=restart_patience, restart_elite=restart_elite,
|
restart_patience=restart_patience, restart_elite=restart_elite,
|
||||||
seed_adjacency_aware=seed_adjacency_aware,
|
seed_adjacency_aware=seed_adjacency_aware,
|
||||||
seed_proportion_aware=seed_proportion_aware,
|
seed_proportion_aware=seed_proportion_aware,
|
||||||
|
|
@ -629,7 +625,6 @@ def search_staged(
|
||||||
inner_kw=inner_kw, log=log, n_workers=n_workers,
|
inner_kw=inner_kw, log=log, n_workers=n_workers,
|
||||||
rank_bonus_fn=lambda root: graph.substrate_readiness(root, reqs, n_storeys),
|
rank_bonus_fn=lambda root: graph.substrate_readiness(root, reqs, n_storeys),
|
||||||
rank_bonus_weight=rank_bonus_weight,
|
rank_bonus_weight=rank_bonus_weight,
|
||||||
tournament_k=tournament_k,
|
|
||||||
niche_by_signature=niche_by_signature,
|
niche_by_signature=niche_by_signature,
|
||||||
restart_patience=restart_patience, restart_elite=restart_elite,
|
restart_patience=restart_patience, restart_elite=restart_elite,
|
||||||
seed_adjacency_aware=seed_adjacency_aware,
|
seed_adjacency_aware=seed_adjacency_aware,
|
||||||
|
|
@ -675,8 +670,7 @@ def search_staged(
|
||||||
# §11.4: the graded objective targets the dense two-floor quality-fail
|
# §11.4: the graded objective targets the dense two-floor quality-fail
|
||||||
# regime, which is Stage 2. Stage 1 keeps its readiness-biased key so the
|
# regime, which is Stage 2. Stage 1 keeps its readiness-biased key so the
|
||||||
# substrate-selection semantics (§11.3) are unchanged.
|
# substrate-selection semantics (§11.3) are unchanged.
|
||||||
use_grade=use_grade, tournament_k=tournament_k,
|
use_grade=use_grade, niche_by_signature=niche_by_signature,
|
||||||
niche_by_signature=niche_by_signature,
|
|
||||||
restart_patience=restart_patience, restart_elite=restart_elite,
|
restart_patience=restart_patience, restart_elite=restart_elite,
|
||||||
enable_reassociate=enable_reassociate,
|
enable_reassociate=enable_reassociate,
|
||||||
feasibility_filter=feasibility_filter,
|
feasibility_filter=feasibility_filter,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue