mergeron 2024.738963.0__py3-none-any.whl → 2025.739265.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mergeron might be problematic. Click here for more details.

Files changed (39) hide show
  1. mergeron/__init__.py +26 -6
  2. mergeron/core/__init__.py +5 -65
  3. mergeron/core/{damodaran_margin_data.py → empirical_margin_distribution.py} +74 -58
  4. mergeron/core/ftc_merger_investigations_data.py +142 -93
  5. mergeron/core/guidelines_boundaries.py +289 -1077
  6. mergeron/core/guidelines_boundary_functions.py +1128 -0
  7. mergeron/core/{guidelines_boundaries_specialized_functions.py → guidelines_boundary_functions_extra.py} +76 -42
  8. mergeron/core/pseudorandom_numbers.py +16 -22
  9. mergeron/data/__init__.py +3 -0
  10. mergeron/data/damodaran_margin_data.xls +0 -0
  11. mergeron/data/damodaran_margin_data_dict.msgpack +0 -0
  12. mergeron/demo/__init__.py +3 -0
  13. mergeron/demo/visualize_empirical_margin_distribution.py +86 -0
  14. mergeron/gen/__init__.py +257 -245
  15. mergeron/gen/data_generation.py +473 -221
  16. mergeron/gen/data_generation_functions.py +876 -0
  17. mergeron/gen/enforcement_stats.py +355 -0
  18. mergeron/gen/upp_tests.py +159 -259
  19. mergeron-2025.739265.0.dist-info/METADATA +115 -0
  20. mergeron-2025.739265.0.dist-info/RECORD +23 -0
  21. {mergeron-2024.738963.0.dist-info → mergeron-2025.739265.0.dist-info}/WHEEL +1 -1
  22. mergeron/License.txt +0 -16
  23. mergeron/core/InCommon RSA Server CA cert chain.pem +0 -68
  24. mergeron/core/excel_helper.py +0 -259
  25. mergeron/core/proportions_tests.py +0 -520
  26. mergeron/ext/__init__.py +0 -5
  27. mergeron/ext/tol_colors.py +0 -851
  28. mergeron/gen/_data_generation_functions_nonpublic.py +0 -621
  29. mergeron/gen/investigations_stats.py +0 -709
  30. mergeron/jinja_LaTex_templates/clrrate_cis_summary_table_template.tex.jinja2 +0 -121
  31. mergeron/jinja_LaTex_templates/ftcinvdata_byhhianddelta_table_template.tex.jinja2 +0 -82
  32. mergeron/jinja_LaTex_templates/ftcinvdata_summary_table_template.tex.jinja2 +0 -57
  33. mergeron/jinja_LaTex_templates/ftcinvdata_summarypaired_table_template.tex.jinja2 +0 -104
  34. mergeron/jinja_LaTex_templates/mergeron.cls +0 -161
  35. mergeron/jinja_LaTex_templates/mergeron_table_collection_template.tex.jinja2 +0 -90
  36. mergeron/jinja_LaTex_templates/setup_tikz_tables.tex.jinja2 +0 -84
  37. mergeron-2024.738963.0.dist-info/METADATA +0 -108
  38. mergeron-2024.738963.0.dist-info/RECORD +0 -30
  39. /mergeron/{core → data}/ftc_invdata.msgpack +0 -0
@@ -0,0 +1,1128 @@
1
+ import decimal
2
+ from dataclasses import dataclass
3
+ from typing import Any, Literal, TypedDict
4
+
5
+ import numpy as np
6
+ from mpmath import mp, mpf # type: ignore
7
+
8
+ from .. import DEFAULT_REC_RATIO, VERSION, ArrayBIGINT, ArrayDouble # noqa: TID252
9
+ from . import MPFloat
10
+
11
+ __version__ = VERSION
12
+
13
+ mp.dps = 32
14
+ mp.trap_complex = True
15
+
16
+
17
+ class ShareRatioBoundaryKeywords(TypedDict, total=False):
18
+ """Keyword arguments for functions generating share ratio boundaries."""
19
+
20
+ recapture_form: Literal["inside-out", "proportional"]
21
+ dps: int
22
+ agg_method: Literal["arithmetic mean", "geometric mean", "distance"]
23
+ weighting: Literal["own-share", "cross-product-share", None]
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class GuidelinesBoundary:
28
+ """Output of a Guidelines boundary function."""
29
+
30
+ coordinates: ArrayDouble
31
+ """Market-share pairs as Cartesian coordinates of points on the boundary."""
32
+
33
+ area: float
34
+ """Area under the boundary."""
35
+
36
+
37
+ def dh_area(_delta_bound: float | MPFloat = 0.01, /, *, dps: int = 9) -> float:
38
+ R"""
39
+ Area under the ΔHHI boundary.
40
+
41
+ When the given ΔHHI bound matches a Guidelines standard,
42
+ the area under the boundary is half the intrinsic clearance rate
43
+ for the ΔHHI safeharbor.
44
+
45
+ Notes
46
+ -----
47
+ To derive the knots, :math:`(s^0_1, s^1_1), (s^1_1, s^0_1)`
48
+ of the ΔHHI boundary, i.e., the points where it intersects
49
+ the merger-to-monopoly boundary, solve
50
+
51
+ .. math::
52
+
53
+ 2 s1 s_2 &= ΔHHI\\
54
+ s_1 + s_2 &= 1
55
+
56
+ Parameters
57
+ ----------
58
+ _delta_bound
59
+ Change in concentration.
60
+ dps
61
+ Specified precision in decimal places.
62
+
63
+ Returns
64
+ -------
65
+ Area under ΔHHI boundary.
66
+
67
+ """
68
+
69
+ _delta_bound = mpf(f"{_delta_bound}")
70
+ _s_naught = (1 - mp.sqrt(1 - 2 * _delta_bound)) / 2
71
+
72
+ return round(
73
+ float(
74
+ _s_naught + (_delta_bound / 2) * (mp.ln(1 - _s_naught) - mp.ln(_s_naught))
75
+ ),
76
+ dps,
77
+ )
78
+
79
+
80
+ def hhi_delta_boundary(
81
+ _delta_bound: float | decimal.Decimal | MPFloat = 0.01, /, *, dps: int = 5
82
+ ) -> GuidelinesBoundary:
83
+ """
84
+ Generate the list of share combination on the ΔHHI boundary.
85
+
86
+ Parameters
87
+ ----------
88
+ _delta_bound:
89
+ Merging-firms' ΔHHI bound.
90
+ dps
91
+ Number of decimal places for rounding reported shares.
92
+
93
+ Returns
94
+ -------
95
+ Array of share-pairs, area under boundary.
96
+
97
+ """
98
+
99
+ _delta_bound = mpf(f"{_delta_bound}")
100
+ _s_naught = 1 / 2 * (1 - mp.sqrt(1 - 2 * _delta_bound))
101
+ _s_mid = mp.sqrt(_delta_bound / 2)
102
+
103
+ _dh_step_sz = mp.power(10, -6)
104
+ _s_1 = np.array(mp.arange(_s_mid, _s_naught - mp.eps, -_dh_step_sz))
105
+
106
+ # Boundary points
107
+ _dh_half = np.vstack((
108
+ np.column_stack((_s_1, _delta_bound / (2 * _s_1))).astype(np.float64),
109
+ np.array([(mpf("0.0"), mpf("1.0"))], np.float64),
110
+ ))
111
+ _dh_bdry_pts = np.vstack((_dh_half[::-1], _dh_half[1:, ::-1]))
112
+
113
+ return GuidelinesBoundary(_dh_bdry_pts, dh_area(_delta_bound, dps=dps))
114
+
115
+
116
+ def hhi_pre_contrib_boundary(
117
+ _hhi_bound: float | decimal.Decimal | MPFloat = 0.03125, /, *, dps: int = 5
118
+ ) -> GuidelinesBoundary:
119
+ """
120
+ Share combinations on the premerger HHI contribution boundary.
121
+
122
+ Parameters
123
+ ----------
124
+ _hhi_bound:
125
+ Merging-firms' pre-merger HHI contribution bound.
126
+ dps
127
+ Number of decimal places for rounding reported shares.
128
+
129
+ Returns
130
+ -------
131
+ Array of share-pairs, area under boundary.
132
+
133
+ """
134
+ _hhi_bound = mpf(f"{_hhi_bound}")
135
+ _s_mid = mp.sqrt(_hhi_bound / 2)
136
+
137
+ _bdry_step_sz = mp.power(10, -dps)
138
+ # Range-limit is 0 less a step, which is -1 * step-size
139
+ _s_1 = np.array(mp.arange(_s_mid, -_bdry_step_sz, -_bdry_step_sz))
140
+ _s_2 = np.sqrt(_hhi_bound - _s_1**2)
141
+ _bdry_pts_mid = np.column_stack((_s_1, _s_2)).astype(np.float64)
142
+
143
+ return GuidelinesBoundary(
144
+ np.vstack((_bdry_pts_mid[::-1], _bdry_pts_mid[1:, ::-1])),
145
+ round(float(mp.pi * _hhi_bound / 4), dps),
146
+ )
147
+
148
+
149
+ def combined_share_boundary(
150
+ _s_intcpt: float | decimal.Decimal | MPFloat = 0.0625, /, *, dps: int = 10
151
+ ) -> GuidelinesBoundary:
152
+ """
153
+ Share combinations on the merging-firms' combined share boundary.
154
+
155
+ Assumes symmetric merging-firm margins. The combined-share is
156
+ congruent to the post-merger HHI contribution boundary, as the
157
+ post-merger HHI bound is the square of the combined-share bound.
158
+
159
+ Parameters
160
+ ----------
161
+ _s_intcpt:
162
+ Merging-firms' combined share.
163
+ dps
164
+ Number of decimal places for rounding reported shares.
165
+
166
+ Returns
167
+ -------
168
+ Array of share-pairs, area under boundary.
169
+
170
+ """
171
+ _s_intcpt = mpf(f"{_s_intcpt}")
172
+ _s_mid = _s_intcpt / 2
173
+
174
+ _s1_pts = np.array([0, _s_mid, _s_intcpt], np.float64)
175
+ return GuidelinesBoundary(
176
+ np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_s_intcpt * _s_mid), dps)
177
+ )
178
+
179
+
180
+ def hhi_post_contrib_boundary(
181
+ _hhi_bound: float | decimal.Decimal | MPFloat = 0.800, /, *, dps: int = 10
182
+ ) -> GuidelinesBoundary:
183
+ """
184
+ Share combinations on the postmerger HHI contribution boundary.
185
+
186
+ The post-merger HHI contribution boundary is identical to the
187
+ combined-share boundary.
188
+
189
+ Parameters
190
+ ----------
191
+ _hhi_bound:
192
+ Merging-firms' pre-merger HHI contribution bound.
193
+ dps
194
+ Number of decimal places for rounding reported shares.
195
+
196
+ Returns
197
+ -------
198
+ Array of share-pairs, area under boundary.
199
+
200
+ """
201
+ return combined_share_boundary(
202
+ _hhi_bound.sqrt()
203
+ if isinstance(_hhi_bound, decimal.Decimal | mpf)
204
+ else np.sqrt(_hhi_bound),
205
+ dps=dps,
206
+ )
207
+
208
+
209
+ def shrratio_boundary_wtd_avg(
210
+ _delta_star: float | decimal.Decimal | MPFloat = 0.075,
211
+ _r_val: float = DEFAULT_REC_RATIO,
212
+ /,
213
+ *,
214
+ agg_method: Literal[
215
+ "arithmetic mean", "geometric mean", "distance"
216
+ ] = "arithmetic mean",
217
+ weighting: Literal["own-share", "cross-product-share", None] = "own-share",
218
+ recapture_form: Literal["inside-out", "proportional"] = "inside-out",
219
+ dps: int = 5,
220
+ ) -> GuidelinesBoundary:
221
+ """
222
+ Share combinations on the share-weighted average diversion ratio boundary.
223
+
224
+ Parameters
225
+ ----------
226
+ _delta_star
227
+ Share ratio (:math:`\\overline{d} / \\overline{r}`)
228
+ _r_val
229
+ recapture ratio
230
+ agg_method
231
+ Whether "arithmetic mean", "geometric mean", or "distance".
232
+ weighting
233
+ Whether "own-share" or "cross-product-share" (or None for simple, unweighted average).
234
+ recapture_form
235
+ Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
236
+ value for both merging firms ("proportional").
237
+ dps
238
+ Number of decimal places for rounding returned shares and area.
239
+
240
+ Returns
241
+ -------
242
+ Array of share-pairs, area under boundary.
243
+
244
+ Notes
245
+ -----
246
+ An analytical expression for the share-weighted arithmetic mean boundary
247
+ is derived and plotted from y-intercept to the ray of symmetry as follows::
248
+
249
+ from sympy import plot as symplot, solve, symbols
250
+ s_1, s_2 = symbols("s_1 s_2", positive=True)
251
+
252
+ g_val, r_val, m_val = 0.06, 0.80, 0.30
253
+ delta_star = g_val / (r_val * m_val)
254
+
255
+ # recapture_form == "inside-out"
256
+ oswag = solve(
257
+ s_1 * s_2 / (1 - s_1)
258
+ + s_2 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
259
+ - (s_1 + s_2) * delta_star,
260
+ s_2
261
+ )[0]
262
+ symplot(
263
+ oswag,
264
+ (s_1, 0., d_hat / (1 + d_hat)),
265
+ ylabel=s_2
266
+ )
267
+
268
+ cpswag = solve(
269
+ s_2 * s_2 / (1 - s_1)
270
+ + s_1 * s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1))
271
+ - (s_1 + s_2) * delta_star,
272
+ s_2
273
+ )[1]
274
+ symplot(
275
+ cpwag,
276
+ (s_1, 0., d_hat / (1 + d_hat)),
277
+ ylabel=s_2
278
+ )
279
+
280
+ # recapture_form == "proportional"
281
+ oswag = solve(
282
+ s_1 * s_2 / (1 - s_1)
283
+ + s_2 * s_1 / (1 - s_2)
284
+ - (s_1 + s_2) * delta_star,
285
+ s_2
286
+ )[0]
287
+ symplot(
288
+ oswag,
289
+ (s_1, 0., d_hat / (1 + d_hat)),
290
+ ylabel=s_2
291
+ )
292
+
293
+ cpswag = solve(
294
+ s_2 * s_2 / (1 - s_1)
295
+ + s_1 * s_1 / (1 - s_2)
296
+ - (s_1 + s_2) * delta_star,
297
+ s_2
298
+ )[1]
299
+ symplot(
300
+ cpswag,
301
+ (s_1, 0.0, d_hat / (1 + d_hat)),
302
+ ylabel=s_2
303
+ )
304
+
305
+
306
+ """
307
+
308
+ _delta_star = mpf(f"{_delta_star}")
309
+ _s_mid = mp.fdiv(_delta_star, 1 + _delta_star)
310
+
311
+ # initial conditions
312
+ _gbdry_points = [(_s_mid, _s_mid)]
313
+ _s_1_pre, _s_2_pre = _s_mid, _s_mid
314
+ _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0.0, 0.0
315
+
316
+ # parameters for iteration
317
+ _gbd_step_sz = mp.power(10, -dps)
318
+ _theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
319
+ for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
320
+ # The wtd. avg. GUPPI is not always convex to the origin, so we
321
+ # increment _s_2 after each iteration in which our algorithm
322
+ # finds (s1, s2) on the boundary
323
+ _s_2 = _s_2_pre * (1 + _theta)
324
+
325
+ if (_s_1 + _s_2) > mpf("0.99875"):
326
+ # Loss of accuracy at 3-9s and up
327
+ break
328
+
329
+ while True:
330
+ _de_1 = _s_2 / (1 - _s_1)
331
+ _de_2 = (
332
+ _s_1 / (1 - lerp(_s_1, _s_2, _r_val))
333
+ if recapture_form == "inside-out"
334
+ else _s_1 / (1 - _s_2)
335
+ )
336
+
337
+ _r = (
338
+ mp.fdiv(
339
+ _s_1 if weighting == "cross-product-share" else _s_2, _s_1 + _s_2
340
+ )
341
+ if weighting
342
+ else 0.5
343
+ )
344
+
345
+ match agg_method:
346
+ case "geometric mean":
347
+ _delta_test = mp.expm1(lerp(mp.log1p(_de_1), mp.log1p(_de_2), _r))
348
+ case "distance":
349
+ _delta_test = mp.sqrt(lerp(_de_1**2, _de_2**2, _r))
350
+ case _:
351
+ _delta_test = lerp(_de_1, _de_2, _r)
352
+
353
+ _test_flag, _incr_decr = (
354
+ (_delta_test > _delta_star, -1)
355
+ if weighting == "cross-product-share"
356
+ else (_delta_test < _delta_star, 1)
357
+ )
358
+
359
+ if _test_flag:
360
+ _s_2 += _incr_decr * _gbd_step_sz
361
+ else:
362
+ break
363
+
364
+ # Build-up boundary points
365
+ _gbdry_points.append((_s_1, _s_2))
366
+
367
+ # Build up area terms
368
+ _s_2_oddsum += _s_2 if _s_2_oddval else 0
369
+ _s_2_evnsum += _s_2 if not _s_2_oddval else 0
370
+ _s_2_oddval = not _s_2_oddval
371
+
372
+ # Hold share points
373
+ _s_2_pre = _s_2
374
+ _s_1_pre = _s_1
375
+
376
+ if _s_2_oddval:
377
+ _s_2_evnsum -= _s_2_pre
378
+ else:
379
+ _s_2_oddsum -= _s_1_pre
380
+
381
+ _s_intcpt = _shrratio_boundary_intcpt(
382
+ _s_2_pre,
383
+ _delta_star,
384
+ _r_val,
385
+ recapture_form=recapture_form,
386
+ agg_method=agg_method,
387
+ weighting=weighting,
388
+ )
389
+
390
+ if weighting == "own-share":
391
+ _gbd_prtlarea = (
392
+ _gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
393
+ )
394
+ # Area under boundary
395
+ _gbdry_area_total = float(
396
+ 2 * (_s_1_pre + _gbd_prtlarea)
397
+ - (mp.power(_s_mid, "2") + mp.power(_s_1_pre, "2"))
398
+ )
399
+
400
+ else:
401
+ _gbd_prtlarea = (
402
+ _gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_intcpt) / 3
403
+ )
404
+ # Area under boundary
405
+ _gbdry_area_total = float(2 * _gbd_prtlarea - mp.power(_s_mid, "2"))
406
+
407
+ _gbdry_points.append((mpf("0.0"), _s_intcpt))
408
+ _gbdry_array = np.array(_gbdry_points).astype(np.float64)
409
+
410
+ # Points defining boundary to point-of-symmetry
411
+ return GuidelinesBoundary(
412
+ np.vstack((_gbdry_array[::-1], _gbdry_array[1:, ::-1])),
413
+ round(float(_gbdry_area_total), dps),
414
+ )
415
+
416
+
417
+ def shrratio_boundary_xact_avg(
418
+ _delta_star: float | decimal.Decimal | MPFloat = 0.075,
419
+ _r_val: float = DEFAULT_REC_RATIO,
420
+ /,
421
+ *,
422
+ recapture_form: Literal["inside-out", "proportional"] = "inside-out",
423
+ dps: int = 5,
424
+ ) -> GuidelinesBoundary:
425
+ """
426
+ Share combinations for the simple average GUPPI boundary with symmetric
427
+ merging-firm margins.
428
+
429
+ Notes
430
+ -----
431
+ An analytical expression for the exact average boundary is derived
432
+ and plotted from the y-intercept to the ray of symmetry as follows::
433
+
434
+ from sympy import latex, plot as symplot, solve, symbols
435
+
436
+ s_1, s_2 = symbols("s_1 s_2")
437
+
438
+ g_val, r_val, m_val = 0.06, 0.80, 0.30
439
+ d_hat = g_val / (r_val * m_val)
440
+
441
+ # recapture_form = "inside-out"
442
+ sag = solve(
443
+ (s_2 / (1 - s_1))
444
+ + (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
445
+ - 2 * d_hat,
446
+ s_2
447
+ )[0]
448
+ symplot(
449
+ sag,
450
+ (s_1, 0., d_hat / (1 + d_hat)),
451
+ ylabel=s_2
452
+ )
453
+
454
+ # recapture_form = "proportional"
455
+ sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
456
+ symplot(
457
+ sag,
458
+ (s_1, 0., d_hat / (1 + d_hat)),
459
+ ylabel=s_2
460
+ )
461
+
462
+ Parameters
463
+ ----------
464
+ _delta_star
465
+ Share ratio (:math:`\\overline{d} / \\overline{r}`).
466
+ _r_val
467
+ Recapture ratio
468
+ recapture_form
469
+ Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
470
+ value for both merging firms ("proportional").
471
+ dps
472
+ Number of decimal places for rounding returned shares.
473
+
474
+ Returns
475
+ -------
476
+ Array of share-pairs, area under boundary, area under boundary.
477
+
478
+ """
479
+
480
+ _delta_star = mpf(f"{_delta_star}")
481
+ _s_mid = _delta_star / (1 + _delta_star)
482
+ _gbd_step_sz = mp.power(10, -dps)
483
+
484
+ _gbdry_points_start = np.array([(_s_mid, _s_mid)])
485
+ _s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
486
+ if recapture_form == "inside-out":
487
+ _s_intcpt = mp.fdiv(
488
+ mp.fsub(
489
+ 2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
490
+ ),
491
+ 2 * mpf(f"{_r_val}"),
492
+ )
493
+ _nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
494
+
495
+ _nr_sqrt_mdr = 4 * _delta_star * _r_val
496
+ _nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
497
+ _nr_sqrt_md2r2 = _nr_sqrt_mdr2 * _delta_star
498
+
499
+ _nr_sqrt_t1 = _nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
500
+ _nr_sqrt_t2 = _nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
501
+ _nr_sqrt_t3 = _nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
502
+ _nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
503
+ _nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
504
+
505
+ _nr_t2_mdr = _nr_sqrt_t1 + _nr_sqrt_t2 + _nr_sqrt_t3 + _nr_sqrt_t4 + _nr_sqrt_t5
506
+
507
+ # Alternative grouping of terms in np.sqrt
508
+ _nr_sqrt_s1sq = (_s_1**2) * (
509
+ _nr_sqrt_md2r2 + _nr_sqrt_mdr2 - _nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
510
+ )
511
+ _nr_sqrt_s1 = _s_1 * (
512
+ -2 * _nr_sqrt_md2r2 - _nr_sqrt_mdr2 + 2 * _nr_sqrt_mdr + 6 * _r_val - 2
513
+ )
514
+ _nr_sqrt_nos1 = _nr_sqrt_md2r2 - _nr_sqrt_mdr + 1
515
+
516
+ _nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
517
+
518
+ if not np.isclose(
519
+ np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
520
+ np.einsum("i->", _nr_t2_s1.astype(np.float64)),
521
+ rtol=0,
522
+ atol=0.5 * dps,
523
+ ):
524
+ raise RuntimeError(
525
+ "Calculation of sq. root term in exact average GUPPI"
526
+ f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
527
+ )
528
+
529
+ _s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
530
+
531
+ else:
532
+ _s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
533
+ _s_2 = (
534
+ (1 / 2)
535
+ + _delta_star
536
+ - _delta_star * _s_1
537
+ - np.sqrt(
538
+ ((_delta_star**2) - 1) * (_s_1**2)
539
+ + (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
540
+ + (_delta_star**2)
541
+ - _delta_star
542
+ + (1 / 4)
543
+ )
544
+ )
545
+
546
+ _gbdry_points_inner = np.column_stack((_s_1, _s_2))
547
+ _gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
548
+
549
+ _gbdry_points = np.vstack((
550
+ _gbdry_points_end,
551
+ _gbdry_points_inner[::-1],
552
+ _gbdry_points_start,
553
+ _gbdry_points_inner[:, ::-1],
554
+ _gbdry_points_end[:, ::-1],
555
+ )).astype(np.float64)
556
+ _s_2 = np.concatenate((np.array([_s_mid], np.float64), _s_2))
557
+
558
+ _gbdry_ends = [0, -1]
559
+ _gbdry_odds = np.array(range(1, len(_s_2), 2), np.int64)
560
+ _gbdry_evns = np.array(range(2, len(_s_2), 2), np.int64)
561
+
562
+ # Double the are under the curve, and subtract the double counted bit.
563
+ _gbdry_area_simpson = 2 * _gbd_step_sz * (
564
+ (4 / 3) * np.sum(_s_2.take(_gbdry_odds))
565
+ + (2 / 3) * np.sum(_s_2.take(_gbdry_evns))
566
+ + (1 / 3) * np.sum(_s_2.take(_gbdry_ends))
567
+ ) - np.power(_s_mid, 2)
568
+
569
+ return GuidelinesBoundary(_gbdry_points, round(float(_gbdry_area_simpson), dps))
570
+
571
+
572
+ def shrratio_boundary_xact_avg_mp(
573
+ _delta_star: float | decimal.Decimal | MPFloat = 0.075,
574
+ _r_val: float = DEFAULT_REC_RATIO,
575
+ /,
576
+ *,
577
+ recapture_form: Literal["inside-out", "proportional"] = "inside-out",
578
+ dps: int = 5,
579
+ ) -> GuidelinesBoundary:
580
+ """
581
+ Share combinations for the simple average GUPPI boundary with symmetric
582
+ merging-firm margins.
583
+
584
+ Notes
585
+ -----
586
+ An analytical expression for the exact average boundary is derived
587
+ and plotted from the y-intercept to the ray of symmetry as follows::
588
+
589
+ from sympy import latex, plot as symplot, solve, symbols
590
+
591
+ s_1, s_2 = symbols("s_1 s_2")
592
+
593
+ g_val, r_val, m_val = 0.06, 0.80, 0.30
594
+ d_hat = g_val / (r_val * m_val)
595
+
596
+ # recapture_form = "inside-out"
597
+ sag = solve(
598
+ (s_2 / (1 - s_1))
599
+ + (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
600
+ - 2 * d_hat,
601
+ s_2
602
+ )[0]
603
+ symplot(
604
+ sag,
605
+ (s_1, 0., d_hat / (1 + d_hat)),
606
+ ylabel=s_2
607
+ )
608
+
609
+ # recapture_form = "proportional"
610
+ sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
611
+ symplot(
612
+ sag,
613
+ (s_1, 0., d_hat / (1 + d_hat)),
614
+ ylabel=s_2
615
+ )
616
+
617
+ Parameters
618
+ ----------
619
+ _delta_star
620
+ Share ratio (:math:`\\overline{d} / \\overline{r}`).
621
+ _r_val
622
+ Recapture ratio
623
+ recapture_form
624
+ Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
625
+ value for both merging firms ("proportional").
626
+ dps
627
+ Number of decimal places for rounding returned shares.
628
+
629
+ Returns
630
+ -------
631
+ Array of share-pairs, area under boundary, area under boundary.
632
+
633
+ """
634
+
635
+ _delta_star = mpf(f"{_delta_star}")
636
+ _s_mid = _delta_star / (1 + _delta_star)
637
+ _gbd_step_sz = mp.power(10, -dps)
638
+
639
+ _gbdry_points_start = np.array([(_s_mid, _s_mid)])
640
+ _s_1 = np.array(mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz), np.float64)
641
+ if recapture_form == "inside-out":
642
+ _s_intcpt = mp.fdiv(
643
+ mp.fsub(
644
+ 2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
645
+ ),
646
+ 2 * mpf(f"{_r_val}"),
647
+ )
648
+ _nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
649
+
650
+ _nr_sqrt_mdr = 4 * _delta_star * _r_val
651
+ _nr_sqrt_mdr2 = _nr_sqrt_mdr * _r_val
652
+ _nr_sqrt_md2r2 = _nr_sqrt_mdr2 * _delta_star
653
+
654
+ _nr_sqrt_t1 = _nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
655
+ _nr_sqrt_t2 = _nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
656
+ _nr_sqrt_t3 = _nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
657
+ _nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
658
+ _nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
659
+
660
+ _nr_t2_mdr = _nr_sqrt_t1 + _nr_sqrt_t2 + _nr_sqrt_t3 + _nr_sqrt_t4 + _nr_sqrt_t5
661
+
662
+ # Alternative grouping of terms in np.sqrt
663
+ _nr_sqrt_s1sq = (_s_1**2) * (
664
+ _nr_sqrt_md2r2 + _nr_sqrt_mdr2 - _nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
665
+ )
666
+ _nr_sqrt_s1 = _s_1 * (
667
+ -2 * _nr_sqrt_md2r2 - _nr_sqrt_mdr2 + 2 * _nr_sqrt_mdr + 6 * _r_val - 2
668
+ )
669
+ _nr_sqrt_nos1 = _nr_sqrt_md2r2 - _nr_sqrt_mdr + 1
670
+
671
+ _nr_t2_s1 = _nr_sqrt_s1sq + _nr_sqrt_s1 + _nr_sqrt_nos1
672
+
673
+ if not np.isclose(
674
+ np.einsum("i->", _nr_t2_mdr.astype(np.float64)),
675
+ np.einsum("i->", _nr_t2_s1.astype(np.float64)),
676
+ rtol=0,
677
+ atol=0.5 * dps,
678
+ ):
679
+ raise RuntimeError(
680
+ "Calculation of sq. root term in exact average GUPPI"
681
+ f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
682
+ )
683
+
684
+ _s_2 = (_nr_t1 - np.sqrt(_nr_t2_s1)) / (2 * _r_val)
685
+
686
+ else:
687
+ _s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
688
+ _s_2 = (
689
+ (1 / 2)
690
+ + _delta_star
691
+ - _delta_star * _s_1
692
+ - np.sqrt(
693
+ ((_delta_star**2) - 1) * (_s_1**2)
694
+ + (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
695
+ + (_delta_star**2)
696
+ - _delta_star
697
+ + (1 / 4)
698
+ )
699
+ )
700
+
701
+ _gbdry_points_inner = np.column_stack((_s_1, _s_2))
702
+ _gbdry_points_end = np.array([(mpf("0.0"), _s_intcpt)], np.float64)
703
+
704
+ _gbdry_points = np.vstack((
705
+ _gbdry_points_end,
706
+ _gbdry_points_inner[::-1],
707
+ _gbdry_points_start,
708
+ np.flip(_gbdry_points_inner, 1),
709
+ np.flip(_gbdry_points_end, 1),
710
+ )).astype(np.float64)
711
+ _s_2 = np.concatenate((np.array([_s_mid], np.float64), _s_2))
712
+
713
+ _gbdry_ends = [0, -1]
714
+ _gbdry_odds = np.array(range(1, len(_s_2), 2), np.int64)
715
+ _gbdry_evns = np.array(range(2, len(_s_2), 2), np.int64)
716
+
717
+ # Double the are under the curve, and subtract the double counted bit.
718
+ _gbdry_area_simpson = 2 * _gbd_step_sz * (
719
+ (4 / 3) * np.sum(_s_2.take(_gbdry_odds))
720
+ + (2 / 3) * np.sum(_s_2.take(_gbdry_evns))
721
+ + (1 / 3) * np.sum(_s_2.take(_gbdry_ends))
722
+ ) - np.power(_s_mid, 2)
723
+
724
+ return GuidelinesBoundary(_gbdry_points, round(float(_gbdry_area_simpson), dps))
725
+
726
+
727
+ def shrratio_boundary_min(
728
+ _delta_star: float | decimal.Decimal | MPFloat = 0.075,
729
+ _r_val: float = DEFAULT_REC_RATIO,
730
+ /,
731
+ *,
732
+ recapture_form: str = "inside-out",
733
+ dps: int = 10,
734
+ ) -> GuidelinesBoundary:
735
+ """
736
+ Share combinations on the minimum GUPPI boundary, with symmetric
737
+ merging-firm margins.
738
+
739
+ Notes
740
+ -----
741
+ With symmetric merging-firm margins, the maximum GUPPI boundary is
742
+ defined by the diversion ratio from the smaller merging-firm to the
743
+ larger one, and is hence unaffected by the method of estimating the
744
+ diversion ratio for the larger firm.
745
+
746
+ Parameters
747
+ ----------
748
+ _delta_star
749
+ Share ratio (:math:`\\overline{d} / \\overline{r}`).
750
+ _r_val
751
+ Recapture ratio.
752
+ recapture_form
753
+ Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
754
+ value for both merging firms ("proportional").
755
+ dps
756
+ Number of decimal places for rounding returned shares.
757
+
758
+ Returns
759
+ -------
760
+ Array of share-pairs, area under boundary.
761
+
762
+ """
763
+
764
+ _delta_star = mpf(f"{_delta_star}")
765
+ _s_intcpt = mpf("1.00")
766
+ _s_mid = _delta_star / (1 + _delta_star)
767
+
768
+ if recapture_form == "inside-out":
769
+ # ## Plot envelope of GUPPI boundaries with r_k = r_bar if s_k = min(_s_1, _s_2)
770
+ # ## See (s_i, s_j) in equation~(44), or thereabouts, in paper
771
+ _smin_nr = _delta_star * (1 - _r_val)
772
+ _smax_nr = 1 - _delta_star * _r_val
773
+ _guppi_bdry_env_dr = _smin_nr + _smax_nr
774
+ _s1_pts = np.array(
775
+ (
776
+ 0,
777
+ _smin_nr / _guppi_bdry_env_dr,
778
+ _s_mid,
779
+ _smax_nr / _guppi_bdry_env_dr,
780
+ _s_intcpt,
781
+ ),
782
+ np.float64,
783
+ )
784
+
785
+ _gbd_area = _s_mid + _s1_pts[1] * (1 - 2 * _s_mid)
786
+ else:
787
+ _s1_pts, _gbd_area = np.array((0, _s_mid, _s_intcpt), np.float64), _s_mid
788
+
789
+ return GuidelinesBoundary(
790
+ np.column_stack((_s1_pts, _s1_pts[::-1])), round(float(_gbd_area), dps)
791
+ )
792
+
793
+
794
+ def shrratio_boundary_max(
795
+ _delta_star: float | decimal.Decimal | MPFloat = 0.075,
796
+ _r_val: float = DEFAULT_REC_RATIO,
797
+ /,
798
+ *,
799
+ dps: int = 10,
800
+ ) -> GuidelinesBoundary:
801
+ """
802
+ Share combinations on the minimum GUPPI boundary with symmetric
803
+ merging-firm margins.
804
+
805
+ Parameters
806
+ ----------
807
+ _delta_star
808
+ Share ratio (:math:`\\overline{d} / \\overline{r}`).
809
+ _r_val
810
+ Recapture ratio.
811
+ dps
812
+ Number of decimal places for rounding returned shares.
813
+
814
+ Returns
815
+ -------
816
+ Array of share-pairs, area under boundary.
817
+
818
+ """
819
+
820
+ # _r_val is not needed for max boundary, but is specified for consistency
821
+ # of function call with other share-ratio boundary functions
822
+ del _r_val
823
+ _delta_star = mpf(f"{_delta_star}")
824
+ _s_intcpt = _delta_star
825
+ _s_mid = _delta_star / (1 + _delta_star)
826
+
827
+ _s1_pts = (0, _s_mid, _s_intcpt)
828
+
829
+ return GuidelinesBoundary(
830
+ np.column_stack((
831
+ np.array(_s1_pts, np.float64),
832
+ np.array(_s1_pts[::-1], np.float64),
833
+ )),
834
+ round(float(_s_intcpt * _s_mid), dps), # simplified calculation
835
+ )
836
+
837
+
838
+ def _shrratio_boundary_intcpt(
839
+ _s_2_pre: float,
840
+ _delta_star: MPFloat,
841
+ _r_val: MPFloat,
842
+ /,
843
+ *,
844
+ recapture_form: Literal["inside-out", "proportional"],
845
+ agg_method: Literal["arithmetic mean", "geometric mean", "distance"],
846
+ weighting: Literal["cross-product-share", "own-share", None],
847
+ ) -> float:
848
+ match weighting:
849
+ case "cross-product-share":
850
+ _s_intcpt: float = _delta_star
851
+ case "own-share":
852
+ _s_intcpt = mpf("1.0")
853
+ case None if agg_method == "distance":
854
+ _s_intcpt = _delta_star * mp.sqrt("2")
855
+ case None if agg_method == "arithmetic mean" and recapture_form == "inside-out":
856
+ _s_intcpt = mp.fdiv(
857
+ mp.fsub(
858
+ 2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
859
+ ),
860
+ 2 * mpf(f"{_r_val}"),
861
+ )
862
+ case None if (
863
+ agg_method == "arithmetic mean" and recapture_form == "proportional"
864
+ ):
865
+ _s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
866
+ case _:
867
+ _s_intcpt = _s_2_pre
868
+
869
+ return _s_intcpt
870
+
871
+
872
+ def lerp[LerpT: (float, MPFloat, ArrayDouble, ArrayBIGINT)](
873
+ _x1: LerpT, _x2: LerpT, _r: float = 0.25, /
874
+ ) -> LerpT:
875
+ """
876
+ From the function of the same name in the C++ standard [2]_
877
+
878
+ Constructs the weighted average, :math:`w_1 x_1 + w_2 x_2`, where
879
+ :math:`w_1 = 1 - r` and :math:`w_2 = r`.
880
+
881
+ Parameters
882
+ ----------
883
+ _x1, _x2
884
+ bounds :math:`x_1, x_2` to interpolate between.
885
+ _r
886
+ interpolation weight :math:`r` assigned to :math:`x_2`
887
+
888
+ Returns
889
+ -------
890
+ The linear interpolation, or weighted average,
891
+ :math:`x_1 + r \\cdot (x_1 - x_2) \\equiv (1 - r) \\cdot x_1 + r \\cdot x_2`.
892
+
893
+ Raises
894
+ ------
895
+ ValueError
896
+ If the interpolation weight is not in the interval, :math:`[0, 1]`.
897
+
898
+ References
899
+ ----------
900
+
901
+ .. [2] C++ Reference, https://en.cppreference.com/w/cpp/numeric/lerp
902
+
903
+ """
904
+
905
+ if not 0 <= _r <= 1:
906
+ raise ValueError("Specified interpolation weight must lie in [0, 1].")
907
+ elif _r == 0:
908
+ return _x1
909
+ elif _r == 1:
910
+ return _x2
911
+ else:
912
+ return _r * _x2 + (1 - _r) * _x1 # pyright: ignore
913
+
914
+
915
+ def round_cust(
916
+ _num: float | decimal.Decimal | MPFloat = 0.060215,
917
+ /,
918
+ *,
919
+ frac: float = 0.005,
920
+ rounding_mode: str = "ROUND_HALF_UP",
921
+ ) -> decimal.Decimal:
922
+ """
923
+ Custom rounding, to the nearest 0.5% by default.
924
+
925
+ Parameters
926
+ ----------
927
+ _num
928
+ Number to be rounded.
929
+ frac
930
+ Fraction to be rounded to.
931
+ rounding_mode
932
+ Rounding mode, as defined in the :code:`decimal` package.
933
+
934
+ Returns
935
+ -------
936
+ The given number, rounded as specified.
937
+
938
+ Raises
939
+ ------
940
+ ValueError
941
+ If rounding mode is not defined in the :code:`decimal` package.
942
+
943
+ Notes
944
+ -----
945
+ Integer-round the quotient, :code:`(_num / frac)` using the specified
946
+ rounding mode. Return the product of the rounded quotient times
947
+ the specified precision, :code:`frac`.
948
+
949
+ """
950
+
951
+ if rounding_mode not in (
952
+ decimal.ROUND_05UP,
953
+ decimal.ROUND_CEILING,
954
+ decimal.ROUND_DOWN,
955
+ decimal.ROUND_FLOOR,
956
+ decimal.ROUND_HALF_DOWN,
957
+ decimal.ROUND_HALF_EVEN,
958
+ decimal.ROUND_HALF_UP,
959
+ decimal.ROUND_UP,
960
+ ):
961
+ raise ValueError(
962
+ f"Value, {f'"{rounding_mode}"'} is invalid for rounding_mode."
963
+ 'Documentation for the, "decimal" built-in lists valid rounding modes.'
964
+ )
965
+
966
+ _n, _f, _e = (decimal.Decimal(f"{_g}") for _g in [_num, frac, 1])
967
+
968
+ return _f * (_n / _f).quantize(_e, rounding=rounding_mode)
969
+
970
+
971
+ def boundary_plot(*, mktshares_plot_flag: bool = True) -> tuple[Any, ...]:
972
+ """Setup basic figure and axes for plots of safe harbor boundaries.
973
+
974
+ See, https://matplotlib.org/stable/tutorials/text/pgf.html
975
+ """
976
+
977
+ import matplotlib as mpl
978
+ import matplotlib.axes as mpa
979
+ import matplotlib.patches as mpp
980
+ import matplotlib.ticker as mpt
981
+
982
+ mpl.use("pgf")
983
+ import matplotlib.pyplot as _plt # noqa: ICN001
984
+
985
+ _plt.rcParams.update({
986
+ "pgf.rcfonts": False,
987
+ "pgf.texsystem": "lualatex",
988
+ "pgf.preamble": "\n".join([
989
+ R"\pdfvariable minorversion=7",
990
+ R"\usepackage{fontspec}",
991
+ R"\usepackage{luacode}",
992
+ R"\begin{luacode}",
993
+ R"local function embedfull(tfmdata)",
994
+ R' tfmdata.embedding = "full"',
995
+ R"end",
996
+ R"",
997
+ R"luatexbase.add_to_callback("
998
+ R' "luaotfload.patch_font", embedfull, "embedfull"'
999
+ R")",
1000
+ R"\end{luacode}",
1001
+ R"\usepackage{mathtools}",
1002
+ R"\usepackage{unicode-math}",
1003
+ R"\setmathfont[math-style=ISO]{STIX Two Math}",
1004
+ R"\setmainfont{STIX Two Text}",
1005
+ r"\setsansfont{Fira Sans Light}",
1006
+ R"\setmonofont[Scale=MatchLowercase,]{Fira Mono}",
1007
+ R"\defaultfontfeatures[\rmfamily]{",
1008
+ R" Ligatures={TeX, Common},",
1009
+ R" Numbers={Proportional, Lining},",
1010
+ R" }",
1011
+ R"\defaultfontfeatures[\sffamily]{",
1012
+ R" Ligatures={TeX, Common},",
1013
+ R" Numbers={Monospaced, Lining},",
1014
+ R" LetterSpace=0.50,",
1015
+ R" }",
1016
+ R"\usepackage[",
1017
+ R" activate={true, nocompatibility},",
1018
+ R" tracking=true,",
1019
+ R" ]{microtype}",
1020
+ ]),
1021
+ })
1022
+
1023
+ # Initialize a canvas with a single figure (set of axes)
1024
+ _fig = _plt.figure(figsize=(5, 5), dpi=600)
1025
+ _ax_out = _fig.add_subplot()
1026
+
1027
+ def _set_axis_def(
1028
+ _ax1: mpa.Axes,
1029
+ /,
1030
+ *,
1031
+ mktshares_plot_flag: bool = False,
1032
+ mktshares_axlbls_flag: bool = False,
1033
+ ) -> mpa.Axes:
1034
+ # Set the width of axis grid lines, and tick marks:
1035
+ # both axes, both major and minor ticks
1036
+ # Frame, grid, and face color
1037
+ for _spos0 in "left", "bottom":
1038
+ _ax1.spines[_spos0].set_linewidth(0.5)
1039
+ _ax1.spines[_spos0].set_zorder(5)
1040
+ for _spos1 in "top", "right":
1041
+ _ax1.spines[_spos1].set_linewidth(0.0)
1042
+ _ax1.spines[_spos1].set_zorder(0)
1043
+ _ax1.spines[_spos1].set_visible(False)
1044
+ _ax1.set_facecolor("#E6E6E6")
1045
+
1046
+ _ax1.grid(linewidth=0.5, linestyle=":", color="grey", zorder=1)
1047
+ _ax1.tick_params(axis="x", which="both", width=0.5)
1048
+ _ax1.tick_params(axis="y", which="both", width=0.5)
1049
+
1050
+ # Tick marks skip, size, and rotation
1051
+ # x-axis
1052
+ _plt.setp(
1053
+ _ax1.xaxis.get_majorticklabels(),
1054
+ horizontalalignment="right",
1055
+ fontsize=6,
1056
+ rotation=45,
1057
+ )
1058
+ # y-axis
1059
+ _plt.setp(
1060
+ _ax1.yaxis.get_majorticklabels(), horizontalalignment="right", fontsize=6
1061
+ )
1062
+
1063
+ if mktshares_plot_flag:
1064
+ # Axis labels
1065
+ if mktshares_axlbls_flag:
1066
+ # x-axis
1067
+ _ax1.set_xlabel("Firm 1 Market Share, $s_1$", fontsize=10)
1068
+ _ax1.xaxis.set_label_coords(0.75, -0.1)
1069
+ # y-axis
1070
+ _ax1.set_ylabel("Firm 2 Market Share, $s_2$", fontsize=10)
1071
+ _ax1.yaxis.set_label_coords(-0.1, 0.75)
1072
+
1073
+ # Plot the ray of symmetry
1074
+ _ax1.plot(
1075
+ [0, 1], [0, 1], linewidth=0.5, linestyle=":", color="grey", zorder=1
1076
+ )
1077
+
1078
+ # Axis scale
1079
+ _ax1.set_xlim(0, 1)
1080
+ _ax1.set_ylim(0, 1)
1081
+ _ax1.set_aspect(1.0)
1082
+
1083
+ # Truncate the axis frame to a triangle:
1084
+ _ax1.add_patch(
1085
+ mpp.Rectangle(
1086
+ xy=(1.0025, 0.00),
1087
+ width=1.1 * mp.sqrt(2),
1088
+ height=1.1 * mp.sqrt(2),
1089
+ angle=45,
1090
+ color="white",
1091
+ edgecolor=None,
1092
+ fill=True,
1093
+ clip_on=True,
1094
+ zorder=5,
1095
+ )
1096
+ )
1097
+ # Feasible space is bounded by the other diagonal:
1098
+ _ax1.plot(
1099
+ [0, 1], [1, 0], linestyle="-", linewidth=0.5, color="black", zorder=1
1100
+ )
1101
+
1102
+ # Axis Tick-mark locations
1103
+ # One can supply an argument to mpt.AutoMinorLocator to
1104
+ # specify a fixed number of minor intervals per major interval, e.g.:
1105
+ # minorLocator = mpt.AutoMinorLocator(2)
1106
+ # would lead to a single minor tick between major ticks.
1107
+ _minorLocator = mpt.AutoMinorLocator(5)
1108
+ _majorLocator = mpt.MultipleLocator(0.05)
1109
+ for _axs in _ax1.xaxis, _ax1.yaxis:
1110
+ if _axs == _ax1.xaxis:
1111
+ _majorticklabels_rot = 45
1112
+ elif _axs == _ax1.yaxis:
1113
+ _majorticklabels_rot = 0
1114
+ # x-axis
1115
+ _axs.set_major_locator(_majorLocator)
1116
+ _axs.set_minor_locator(_minorLocator)
1117
+ # It"s always x when specifying the format
1118
+ _axs.set_major_formatter(mpt.StrMethodFormatter("{x:>3.0%}"))
1119
+
1120
+ # Hide every other tick-label
1121
+ for _axl in _ax1.get_xticklabels(), _ax1.get_yticklabels():
1122
+ _plt.setp(_axl[::2], visible=False)
1123
+
1124
+ return _ax1
1125
+
1126
+ _ax_out = _set_axis_def(_ax_out, mktshares_plot_flag=mktshares_plot_flag)
1127
+
1128
+ return _plt, _fig, _ax_out, _set_axis_def