ASSEMBLY-WP-0016-T02: endogenous foreign shares and split calibration hook (domestic segment with own counters); share-shifting levers effective but modest; CrossBorder § 8

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 14913@bnt-lap001
Assistant-Session: add7ef69-b901-4756-9a67-34cf1125d96f
This commit is contained in:
tegwick 2026-10-13 22:59:32 +02:00
parent 8bb8c8fd4b
commit 824187da3b
6 changed files with 159 additions and 7 deletions

View file

@ -325,3 +325,50 @@ Caveats: export demand scales only with Argentina's own K_Ω (no price or
competition effects, no partner demand). Adding it at t 0 shifts the
calibrated rest point slightly; this is negligible at 1.5 % and noticeable at
10 %.
## 8. Endogenous foreign shares and the split calibration hook (WP-0016-T02)
**Endogenous shares.** `world.make(..., endo=...)` lets each foreign share f
relax (speed λ) toward
f* = f0 · (1 + γ · Δ frontier lead) · (1 − δ · Δ(X · N))
that is, toward more foreign use when the home's frontier lead grows and less
when domestic alternatives (commons X, diversity N) grow. At the start f* = f0,
so the measured shares are a rest point. Priors: λ = 0.1 per year, γ = δ = 1.
**Split calibration hook.** The first hook set the domestic and operator parts
of each channel to the same measured level. That made every share-shifting
lever (portability, endogenous shares) ineffective by construction: moving
users between equally concentrated segments changes nothing. With
`d0 = {channel: domestic-segment concentration}`, the domestic part starts at
d0 with its own counter coefficients, so it is at rest there. The operator
part is solved so that the total keeps its measured level. EU priors: search
0.5 (Ecosia, Qwant: 1–2 % share, one or two firms), cloud 0.3 (EU providers,
about 15 %, several firms), lineages 0.5 (Mistral, open-weight models).
Mobile OS has no EU segment.
| EU lever (coupled, t 100) | EU P | Δ vs. current |
|---|---|---|
| endogenous shares, current levers | 0.818 | −0.003 |
| commons push (IX × 3), fixed shares | 0.807 | −0.014 |
| split hook + commons push + endogenous shares | **0.793** | −0.027 |
| split hook + lineage portability 0.5 | **0.802** | −0.019 |
| (for comparison) strong access rule on Pd, Pmem | 0.683 | −0.138 |
| (for comparison) US moves to J1 levers | 0.635 | −0.186 |
Readings:
1. **Share-shifting levers work once the domestic segment is measured
separately.** Portability and commons-driven substitution lower EU
domination, but by a fifth to a tenth of what access rules or the home's
levers achieve.
2. **The measured foreign shares are close to rest under current levers**
(−0.003 over 100 years): nothing in today's dynamics shifts users toward
domestic alternatives by itself.
3. **The domestic-segment levels d0 are priors.** They need indicator CB3
(open-weight downloads by developer) and the domestic-provider shares in
CB1 and CB4.
Calibration note: runs without `d0` keep the original hook, so all earlier
results are unchanged.

View file

@ -36,6 +36,12 @@ STRONG = {"Pd": 0.15, "Pmem": 0.15} # counterfactual strong market-access
DMA_EXCESS = -0.017
DMA_EXCESS_RANGE = (-0.034, 0.0)
FLOOR = 0.70
ENDO = {"lam": 0.1, "gamma": 1.0, "delta": 1.0}
# Domestic-segment concentration (priors, WP-0016-T02): EU-own search engines (Ecosia, Qwant; 1-2 %
# share) dominated by one or two firms; EU cloud providers (about 15 %) spread over several; domestic
# and open-weight lineages (Mistral, open models). No EU mobile OS, so Pmem has no domestic segment.
D0_EU = {"Pd": 0.5, "Pi": 0.3, "Plin": 0.5}
IX_EU = 0.02 # gadm02 default commons investment; the push triples it
def base() -> dict:
@ -46,7 +52,8 @@ def base() -> dict:
def world(cal: dict, coupled: bool = True, us_lev: dict | None = None, eu_access: dict | None = None,
eu_lev: dict | None = None, eu_port: float = 0.0) -> dict:
eu_lev: dict | None = None, eu_port: float = 0.0, eu_endo: dict | None = None,
eu_d0: dict | None = None) -> dict:
us_p, us_s = cal["US"]
eu_p, eu_s = cal["EU"]
ar_p, ar_s = cal["AR"]
@ -54,7 +61,8 @@ def world(cal: dict, coupled: bool = True, us_lev: dict | None = None, eu_access
"US": w.make({**us_p, **(us_lev or {})}, us_s),
"EU": w.make({**eu_p, **(eu_lev or {})}, eu_s, home="US" if coupled else None,
f=F["EU"] if coupled else None, access=eu_access, kappa=KAPPA if coupled else 0.0,
port=eu_port if coupled else 0.0),
port=eu_port if coupled else 0.0, endo=eu_endo if coupled else None,
d0=eu_d0 if coupled else None),
"AR": w.make(ar_p, ar_s, home="US" if coupled else None, f=F["AR"] if coupled else None,
kappa=KAPPA if coupled else 0.0),
}
@ -100,6 +108,15 @@ def cases(a_fit: float) -> list:
("coupled, EU cloud switching (Data Act) a 0.15 on Pi", dict(eu_access={"Pi": 0.15})),
("coupled, EU all host rules (Pd, Pmem, Plin, Pi 0.15; portability 0.5)",
dict(eu_access={"Pd": 0.15, "Pmem": 0.15, "Plin": 0.15, "Pi": 0.15}, eu_port=0.5)),
# WP-0016-T02: endogenous foreign shares (priors: speed 0.1/y, weights 1)
("coupled, endogenous foreign shares, current levers", dict(eu_endo=ENDO)),
("coupled, EU commons push (IX × 3), fixed shares", dict(eu_lev={"IX": 3 * IX_EU})),
("coupled, EU commons push (IX × 3), endogenous shares", dict(eu_lev={"IX": 3 * IX_EU}, eu_endo=ENDO)),
("coupled, split hook (domestic segment measured), current levers", dict(eu_d0=D0_EU)),
("coupled, split hook, endogenous shares", dict(eu_d0=D0_EU, eu_endo=ENDO)),
("coupled, split hook, commons push, endogenous shares",
dict(eu_d0=D0_EU, eu_endo=ENDO, eu_lev={"IX": 3 * IX_EU})),
("coupled, split hook, lineage portability 0.5", dict(eu_d0=D0_EU, eu_port=0.5)),
]

View file

@ -53,6 +53,27 @@ at most a ≈ 0.006. Counterfactual strong rule: a 0.15.
| coupled, EU all host rules (Pd, Pmem, Plin, Pi 0.15; portability 0.5) | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, EU all host rules (Pd, Pmem, Plin, Pi 0.15; portability 0.5) | EU | 0.72 → 0.43 → 0.35 | 0.14 / 0.07 / 0.57 | 0.41 | 0.438 | 0.53 | 0.34 | ok |
| coupled, EU all host rules (Pd, Pmem, Plin, Pi 0.15; portability 0.5) | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.237 | 0.34 | 0.26 | broken |
| coupled, endogenous foreign shares, current levers | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, endogenous foreign shares, current levers | EU | 0.72 → 0.80 → 0.82 | 0.88 / 0.63 / 0.90 | 0.30 | 0.282 | 0.41 | 0.42 | broken |
| coupled, endogenous foreign shares, current levers | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.241 | 0.35 | 0.27 | broken |
| coupled, EU commons push (IX × 3), fixed shares | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, EU commons push (IX × 3), fixed shares | EU | 0.72 → 0.79 → 0.81 | 0.88 / 0.63 / 0.91 | 0.33 | 0.328 | 0.42 | 0.40 | broken |
| coupled, EU commons push (IX × 3), fixed shares | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.241 | 0.35 | 0.27 | broken |
| coupled, EU commons push (IX × 3), endogenous shares | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, EU commons push (IX × 3), endogenous shares | EU | 0.72 → 0.79 → 0.81 | 0.88 / 0.63 / 0.90 | 0.33 | 0.327 | 0.42 | 0.40 | broken |
| coupled, EU commons push (IX × 3), endogenous shares | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.241 | 0.35 | 0.27 | broken |
| coupled, split hook (domestic segment measured), current levers | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, split hook (domestic segment measured), current levers | EU | 0.72 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.30 | 0.283 | 0.41 | 0.42 | broken |
| coupled, split hook (domestic segment measured), current levers | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.241 | 0.35 | 0.27 | broken |
| coupled, split hook, endogenous shares | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, split hook, endogenous shares | EU | 0.72 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.30 | 0.282 | 0.41 | 0.42 | broken |
| coupled, split hook, endogenous shares | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.241 | 0.35 | 0.27 | broken |
| coupled, split hook, commons push, endogenous shares | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, split hook, commons push, endogenous shares | EU | 0.72 → 0.78 → 0.79 | 0.88 / 0.63 / 0.90 | 0.33 | 0.332 | 0.42 | 0.40 | broken |
| coupled, split hook, commons push, endogenous shares | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.241 | 0.35 | 0.27 | broken |
| coupled, split hook, lineage portability 0.5 | US | 0.68 → 0.79 → 0.82 | 0.88 / 0.63 / 0.90 | 0.36 | 0.253 | 0.36 | 0.32 | broken |
| coupled, split hook, lineage portability 0.5 | EU | 0.68 → 0.77 → 0.80 | 0.88 / 0.63 / 0.86 | 0.31 | 0.293 | 0.41 | 0.41 | broken |
| coupled, split hook, lineage portability 0.5 | AR | 0.75 → 0.81 → 0.83 | 0.88 / 0.63 / 0.90 | 0.32 | 0.241 | 0.35 | 0.27 | broken |
## EU domination at t 100: effect of each lever (coupled model)
@ -70,3 +91,10 @@ at most a ≈ 0.006. Counterfactual strong rule: a 0.15.
| coupled, EU lineage access 0.15 + portability 0.5 | 0.757 | -0.064 |
| coupled, EU cloud switching (Data Act) a 0.15 on Pi | 0.766 | -0.055 |
| coupled, EU all host rules (Pd, Pmem, Plin, Pi 0.15; portability 0.5) | 0.350 | -0.471 |
| coupled, endogenous foreign shares, current levers | 0.818 | -0.003 |
| coupled, EU commons push (IX × 3), fixed shares | 0.807 | -0.014 |
| coupled, EU commons push (IX × 3), endogenous shares | 0.807 | -0.014 |
| coupled, split hook (domestic segment measured), current levers | 0.817 | -0.004 |
| coupled, split hook, endogenous shares | 0.818 | -0.003 |
| coupled, split hook, commons push, endogenous shares | 0.793 | -0.027 |
| coupled, split hook, lineage portability 0.5 | 0.802 | -0.019 |

View file

@ -74,6 +74,24 @@ class TestWorld(unittest.TestCase):
self.assertLess(port["host"][-1]["Plin"], base["host"][-1]["Plin"])
self.assertAlmostEqual(port["home"][-1]["Plin"], base["home"][-1]["Plin"], places=12)
def test_endogenous_shares_rest_at_start_and_fall_with_domestic_alternatives(self):
f = {c: 0.8 for c in w.COUPLED}
endo = {"lam": 0.5, "gamma": 0.0, "delta": 2.0}
still = w.simulate_world({"home": w.make(), "host": w.make(home="home", f=f, endo=endo)}, T=1, record_every=1)
self.assertAlmostEqual(still["host"][1]["f_Pd"], 0.8, places=3) # near rest at start
push = w.simulate_world({"home": w.make(), "host": w.make({"IX": 0.3}, home="home", f=f, endo=endo)}, T=T)
fixed = w.simulate_world({"home": w.make(), "host": w.make({"IX": 0.3}, home="home", f=f)}, T=T)
self.assertLess(push["host"][-1]["f_Pd"], 0.8)
self.assertEqual(fixed["host"][-1]["f_Pd"], 0.8)
def test_split_hook_keeps_measured_level(self):
f = {c: 0.8 for c in w.COUPLED}
j = w.make(home="home", f=f, d0={"Pd": 0.3})
self.assertAlmostEqual(w.mixed(j)["Pd"], w.make(home="home", f=f)["s"]["Pd"], places=12)
self.assertEqual(j["s"]["Pd"], 0.3)
d, _a, _sm = w.step_derivatives({"home": w.make(), "host": j})["host"]
self.assertLess(abs(d["Pd"]), 1e-3) # the domestic segment starts at rest
if __name__ == "__main__":
unittest.main()

View file

@ -34,20 +34,39 @@ OP = {c: "O" + c for c in COUPLED} # operator-part stocks
def make(params: dict | None = None, init: dict | None = None, home: str | None = None,
f: dict | None = None, access: dict | None = None, kappa: float = 0.0, port: float = 0.0) -> dict:
f: dict | None = None, access: dict | None = None, kappa: float = 0.0, port: float = 0.0,
endo: dict | None = None, d0: dict | None = None) -> dict:
"""One jurisdiction: gadm04 params/init plus coupling settings.
home: name of the operators' home jurisdiction (None = no foreign operators);
f: foreign share per coupled channel; access: host market-access rule per channel;
port: lineage portability rule (WP-0016-T01), 0..1: share of imported-lineage use that users can
move to domestic or open lineages; lowers the effective lineage import share f_Plin * (1 - port).
endo: endogenous foreign shares (WP-0016-T02), {"lam": speed per year, "gamma": weight of the
home's frontier lead, "delta": weight of domestic alternatives X*N}. f relaxes toward
f* = f0 * (1 + gamma * (lead - lead0)) * (1 - delta * (alt - alt0)), clipped to [0, 1]; at the
start f* = f0, so the measured shares are a rest point.
d0: measured concentration of the domestic segment per channel (WP-0016-T02). Without it, the
calibration hook sets D(0) = O(0) = P(0), which makes every share-shifting lever ineffective.
With it, D(0) = d0 and O(0) is solved so that the mixed channel keeps its measured P(0).
"""
p = {**g.PARAMS, **(params or {})}
s = {**g.INIT, **(init or {})}
fs = {c: 0.0 for c in COUPLED} | (f or {})
for c in COUPLED:
s[OP[c]] = s[c] # calibration hook: D(0) = O(0) = P(0)
return {"p": p, "s": s, "home": home, "f": {c: 0.0 for c in COUPLED} | (f or {}),
"access": {c: 0.0 for c in COUPLED} | (access or {}), "kappa": kappa, "port": port}
if d0 and c in d0 and fs[c] > 0: # split: domestic segment measured, operator part keeps P(0)
total = s[c]
fc = fs[c] * (1 - port) if c == "Plin" else fs[c]
s[c] = d0[c]
s[OP[c]] = min(1.0, max(0.0, (total - (1 - fc) * d0[c]) / fc))
dom = None
if d0: # the domestic segment gets its own channel counters, so that D(0) = d0 is a rest point
coef = g.calibrate(p, s)
dom = {g3.COEF[c]: coef[g3.COEF[c]] for c in COUPLED if c in d0}
return {"p": p, "s": s, "home": home, "f": {c: 0.0 for c in COUPLED} | (f or {}), "dom": dom,
"access": {c: 0.0 for c in COUPLED} | (access or {}), "kappa": kappa, "port": port,
"endo": endo}
def context(rows: list[dict], params: dict | None = None) -> dict:
@ -94,7 +113,7 @@ def step_derivatives(world: dict, sigma: dict | None = None) -> dict:
sm = mixed(j)
d, a = g.derivatives(sm, p) # everything the host sees
if any(j["f"][c] for c in COUPLED): # domestic channel parts at their own level
dd, _a = g.derivatives(j["s"], p)
dd, _a = g.derivatives(j["s"], {**p, **(j.get("dom") or {})})
for c in COUPLED:
d[c] = dd[c]
for c in COUPLED: # global spillover acts on domestic parts too
@ -130,6 +149,11 @@ def simulate_world(world: dict, T: float = 100.0, dt: float = 0.1, record_every:
_d, a0 = g.derivatives(mixed(j), j["p"])
j["p"] = {**j["p"], "sig_pb0": g.perceived_benefit(mixed(j), a0, g.legislated(mixed(j), j["p"]))}
keys = g.STOCKS + tuple(OP.values())
for name, j in world.items():
if j.get("endo") and j["home"] is not None:
h = world[j["home"]]["s"]
j["f"] = dict(j["f"])
j["_ref"] = {"f0": dict(j["f"]), "lead0": h["Ko"] - j["s"]["Ko"], "alt0": j["s"]["X"] * j["s"]["N"]}
rows = {k: [] for k in world}
for j in world.values():
if j.get("path") and record_every and len(j["path"]) < int(round(T / dt)) + 1:
@ -143,9 +167,21 @@ def simulate_world(world: dict, T: float = 100.0, dt: float = 0.1, record_every:
**{k: a[k] for k in ("H", "A", "n_ch", "n_w", "n_s", "n_d", "care_need",
"Ystar", "B", "V", "Vsel", "Hch", "Hw", "Hs", "Hd")}}
row.update({OP[c]: world[name]["s"][OP[c]] for c in COUPLED})
row.update({"f_" + c: world[name]["f"][c] for c in COUPLED})
row["VFAC"] = g2.vfac(row)
rows[name].append(row)
if i < steps:
for name, j in world.items(): # endogenous foreign shares (previous state)
e = j.get("endo")
if not e or j["home"] is None:
continue
ref, h = j["_ref"], world[j["home"]]["s"]
lead, alt = h["Ko"] - j["s"]["Ko"], j["s"]["X"] * j["s"]["N"]
for c in COUPLED:
target = ref["f0"][c] * (1 + e.get("gamma", 1.0) * (lead - ref["lead0"])) \
* (1 - e.get("delta", 1.0) * (alt - ref["alt0"]))
target = min(1.0, max(0.0, target))
j["f"][c] = min(1.0, max(0.0, j["f"][c] + dt * e.get("lam", 0.1) * (target - j["f"][c])))
for name, (d, _a, _sm) in der.items():
if world[name].get("path"):
row = world[name]["path"][min(i + 1, len(world[name]["path"]) - 1)]

View file

@ -42,11 +42,17 @@ after interoperability rules, or the AI Act's GPAI obligations). Re-run
```task
id: ASSEMBLY-WP-0016-T02
status: todo
status: done
priority: medium
state_hub_task_id: "7a89281d-0cba-5d3f-85c6-6e29cb4f2de8"
```
**Done 2026-10-13:** endogenous foreign shares (`endo`) and the split
calibration hook (`d0`, domestic segment with its own counters), tests,
`models/CrossBorder.md` § 8. Share-shifting levers work only with the split
hook: EU P −0.019 (portability), −0.027 (commons push with endogenous shares).
Priors λ, γ, δ and d0 still need data (CB1, CB3, CB4).
Make f_c^j a state: it falls when domestic alternatives grow (domestic X,
N, open lineages) or access rules bind, and rises with the operator's
quality lead (frontier gap). Check that today's measured f (§ 2) is a rest