mergeron 2025.739290.3__py3-none-any.whl → 2025.739290.5__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.

@@ -8,16 +8,16 @@ poor performance
8
8
  """
9
9
 
10
10
  from collections.abc import Callable
11
- from dataclasses import dataclass
12
11
  from typing import Literal
13
12
 
14
13
  import numpy as np
14
+ from attrs import frozen
15
15
  from mpmath import mp, mpf # type: ignore
16
16
  from scipy.spatial.distance import minkowski as distance_function # type: ignore
17
17
  from sympy import lambdify, simplify, solve, symbols # type: ignore
18
18
 
19
19
  from .. import DEFAULT_REC_RATIO, VERSION, ArrayDouble # noqa: TID252
20
- from . import guidelines_boundary_functions as gbfn
20
+ from . import guidelines_boundary_functions as gbf
21
21
 
22
22
  __version__ = VERSION
23
23
 
@@ -26,14 +26,14 @@ mp.dps = 32
26
26
  mp.trap_complex = True
27
27
 
28
28
 
29
- @dataclass(slots=True, frozen=True)
29
+ @frozen
30
30
  class GuidelinesBoundaryCallable:
31
31
  boundary_function: Callable[[ArrayDouble], ArrayDouble]
32
32
  area: float
33
33
  s_naught: float = 0
34
34
 
35
35
 
36
- def dh_area_quad(_dh_val: float = 0.01, /, *, dps: int = 9) -> float:
36
+ def dh_area_quad(_dh_val: float = 0.01, /) -> float:
37
37
  """
38
38
  Area under the ΔHHI boundary.
39
39
 
@@ -45,8 +45,6 @@ def dh_area_quad(_dh_val: float = 0.01, /, *, dps: int = 9) -> float:
45
45
  ----------
46
46
  _dh_val
47
47
  Merging-firms' ΔHHI bound.
48
- dps
49
- Specified precision in decimal places.
50
48
 
51
49
  Returns
52
50
  -------
@@ -57,11 +55,12 @@ def dh_area_quad(_dh_val: float = 0.01, /, *, dps: int = 9) -> float:
57
55
  _dh_val = mpf(f"{_dh_val}")
58
56
  _s_naught = (1 - mp.sqrt(1 - 2 * _dh_val)) / 2
59
57
 
60
- return round(
61
- float(
62
- _s_naught + mp.quad(lambda x: _dh_val / (2 * x), [_s_naught, 1 - _s_naught])
63
- ),
64
- dps,
58
+ return float(
59
+ mp.nstr(
60
+ _s_naught
61
+ + mp.quad(lambda x: _dh_val / (2 * x), [_s_naught, 1 - _s_naught]),
62
+ mp.prec,
63
+ )
65
64
  )
66
65
 
67
66
 
@@ -84,18 +83,24 @@ def hhi_delta_boundary_qdtr(_dh_val: float = 0.01, /) -> GuidelinesBoundaryCalla
84
83
 
85
84
  _s_1, _s_2 = symbols("s_1, s_2", positive=True)
86
85
 
87
- _hhi_eqn = _s_2 - 0.01 / (2 * _s_1)
86
+ _hhi_eqn = _s_2 - _dh_val / (2 * _s_1)
88
87
 
89
- _hhi_bdry = solve(_hhi_eqn, _s_2)[0]
90
- _s_nought = float(solve(_hhi_eqn.subs({_s_2: 1 - _s_1}), _s_1)[0])
88
+ hhi_bdry = solve(_hhi_eqn, _s_2)[0]
89
+ s_nought = float(solve(_hhi_eqn.subs({_s_2: 1 - _s_1}), _s_1)[0])
91
90
 
92
- _hhi_bdry_area = 2 * (
93
- _s_nought
94
- + mp.quad(lambdify(_s_1, _hhi_bdry, "mpmath"), (_s_nought, 1 - _s_nought))
91
+ hhi_bdry_area = float(
92
+ mp.nstr(
93
+ 2
94
+ * (
95
+ s_nought
96
+ + mp.quad(lambdify(_s_1, hhi_bdry, "mpmath"), (s_nought, 1 - s_nought))
97
+ ),
98
+ mp.prec,
99
+ )
95
100
  )
96
101
 
97
102
  return GuidelinesBoundaryCallable(
98
- lambdify(_s_1, _hhi_bdry, "numpy"), _hhi_bdry_area, _s_nought
103
+ lambdify(_s_1, hhi_bdry, "numpy"), hhi_bdry_area, s_nought
99
104
  )
100
105
 
101
106
 
@@ -131,62 +136,60 @@ def shrratio_boundary_qdtr_wtd_avg(
131
136
 
132
137
  _delta_star = mpf(f"{_delta_star}")
133
138
  _s_mid = _delta_star / (1 + _delta_star)
134
- _s_naught = 0
139
+ s_naught = 0
135
140
 
136
- _s_1, _s_2 = symbols("s_1:3", positive=True)
141
+ s_1, s_2 = symbols("s_1:3", positive=True)
137
142
 
138
143
  match weighting:
139
144
  case "own-share":
140
145
  _bdry_eqn = (
141
- _s_1 * _s_2 / (1 - _s_1)
142
- + _s_2
143
- * _s_1
146
+ s_1 * s_2 / (1 - s_1)
147
+ + s_2
148
+ * s_1
144
149
  / (
145
- (1 - (_r_val * _s_2 + (1 - _r_val) * _s_1))
150
+ (1 - (_r_val * s_2 + (1 - _r_val) * s_1))
146
151
  if recapture_form == "inside-out"
147
- else (1 - _s_2)
152
+ else (1 - s_2)
148
153
  )
149
- - (_s_1 + _s_2) * _delta_star
154
+ - (s_1 + s_2) * _delta_star
150
155
  )
151
156
 
152
- _bdry_func = solve(_bdry_eqn, _s_2)[0]
153
- _s_naught = (
154
- float(solve(simplify(_bdry_eqn.subs({_s_2: 1 - _s_1})), _s_1)[0]) # type: ignore
157
+ _bdry_func = solve(_bdry_eqn, s_2)[0]
158
+ s_naught = (
159
+ float(solve(simplify(_bdry_eqn.subs({s_2: 1 - s_1})), s_1)[0]) # type: ignore
155
160
  if recapture_form == "inside-out"
156
161
  else 0
157
162
  )
158
- _bdry_area = float(
163
+ bdry_area = float(
159
164
  2
160
165
  * (
161
- _s_naught
162
- + mp.quad(lambdify(_s_1, _bdry_func, "mpmath"), (_s_naught, _s_mid))
166
+ s_naught
167
+ + mp.quad(lambdify(s_1, _bdry_func, "mpmath"), (s_naught, _s_mid))
163
168
  )
164
- - (_s_mid**2 + _s_naught**2)
169
+ - (_s_mid**2 + s_naught**2)
165
170
  )
166
171
 
167
172
  case "cross-product-share":
168
173
  mp.trap_complex = False
169
- _d_star = symbols("d", positive=True)
174
+ d_star = symbols("d", positive=True)
170
175
  _bdry_eqn = (
171
- _s_2 * _s_2 / (1 - _s_1)
172
- + _s_1
173
- * _s_1
176
+ s_2 * s_2 / (1 - s_1)
177
+ + s_1
178
+ * s_1
174
179
  / (
175
- (1 - (_r_val * _s_2 + (1 - _r_val) * _s_1))
180
+ (1 - (_r_val * s_2 + (1 - _r_val) * s_1))
176
181
  if recapture_form == "inside-out"
177
- else (1 - _s_2)
182
+ else (1 - s_2)
178
183
  )
179
- - (_s_1 + _s_2) * _d_star
184
+ - (s_1 + s_2) * d_star
180
185
  )
181
186
 
182
- _bdry_func = solve(_bdry_eqn, _s_2)[1]
183
- _bdry_area = float(
187
+ _bdry_func = solve(_bdry_eqn, s_2)[1]
188
+ bdry_area = float(
184
189
  2
185
190
  * (
186
191
  mp.quad(
187
- lambdify(
188
- _s_1, _bdry_func.subs({_d_star: _delta_star}), "mpmath"
189
- ),
192
+ lambdify(s_1, _bdry_func.subs({d_star: _delta_star}), "mpmath"),
190
193
  (0, _s_mid),
191
194
  )
192
195
  ).real
@@ -195,30 +198,30 @@ def shrratio_boundary_qdtr_wtd_avg(
195
198
 
196
199
  case _:
197
200
  _bdry_eqn = (
198
- 1 / 2 * _s_2 / (1 - _s_1)
201
+ 1 / 2 * s_2 / (1 - s_1)
199
202
  + 1
200
203
  / 2
201
- * _s_1
204
+ * s_1
202
205
  / (
203
- (1 - (_r_val * _s_2 + (1 - _r_val) * _s_1))
206
+ (1 - (_r_val * s_2 + (1 - _r_val) * s_1))
204
207
  if recapture_form == "inside-out"
205
- else (1 - _s_2)
208
+ else (1 - s_2)
206
209
  )
207
210
  - _delta_star
208
211
  )
209
212
 
210
- _bdry_func = solve(_bdry_eqn, _s_2)[0]
211
- _bdry_area = float(
212
- 2 * (mp.quad(lambdify(_s_1, _bdry_func, "mpmath"), (0, _s_mid)))
213
+ _bdry_func = solve(_bdry_eqn, s_2)[0]
214
+ bdry_area = float(
215
+ 2 * (mp.quad(lambdify(s_1, _bdry_func, "mpmath"), (0, _s_mid)))
213
216
  - _s_mid**2
214
217
  )
215
218
 
216
219
  return GuidelinesBoundaryCallable(
217
- lambdify(_s_1, _bdry_func, "numpy"), _bdry_area, _s_naught
220
+ lambdify(s_1, _bdry_func, "numpy"), bdry_area, s_naught
218
221
  )
219
222
 
220
223
 
221
- def shrratio_boundary_distance(
224
+ def shrratio_boundary_distance( # noqa: PLR0914
222
225
  _delta_star: float = 0.075,
223
226
  _r_val: float = DEFAULT_REC_RATIO,
224
227
  /,
@@ -227,7 +230,7 @@ def shrratio_boundary_distance(
227
230
  weighting: Literal["own-share", "cross-product-share"] | None = "own-share",
228
231
  recapture_form: Literal["inside-out", "proportional"] = "inside-out",
229
232
  dps: int = 5,
230
- ) -> gbfn.GuidelinesBoundary:
233
+ ) -> gbf.GuidelinesBoundary:
231
234
  """
232
235
  Share combinations for the GUPPI boundaries using various aggregators with
233
236
  symmetric merging-firm margins.
@@ -261,42 +264,41 @@ def shrratio_boundary_distance(
261
264
  """
262
265
 
263
266
  _delta_star = mpf(f"{_delta_star}")
264
- _s_mid = _delta_star / (1 + _delta_star)
265
-
266
- # initial conditions
267
- _gbdry_points = [(_s_mid, _s_mid)]
268
- _s_1_pre, _s_2_pre = _s_mid, _s_mid
269
- _s_2_oddval, _s_2_oddsum, _s_2_evnsum = True, 0.0, 0.0
270
267
 
271
268
  # parameters for iteration
269
+ _s_mid = mp.fdiv(_delta_star, 1 + _delta_star)
272
270
  _weights_base = (mpf("0.5"),) * 2
273
- _gbd_step_sz = mp.power(10, -dps)
274
- _theta = _gbd_step_sz * (10 if weighting == "cross-product-share" else 1)
275
- for _s_1 in mp.arange(_s_mid - _gbd_step_sz, 0, -_gbd_step_sz):
271
+ _bdry_step_sz = mp.power(10, -dps)
272
+ _theta = _bdry_step_sz * (10 if weighting == "cross-product-share" else 1)
273
+
274
+ # initial conditions
275
+ bdry_points = [(_s_mid, _s_mid)]
276
+ s_1_pre, s_2_pre = _s_mid, _s_mid
277
+ s_2_oddval, s_2_oddsum, s_2_evnsum = True, 0.0, 0.0
278
+ for s_1 in mp.arange(_s_mid - _bdry_step_sz, 0, -_bdry_step_sz):
276
279
  # The wtd. avg. GUPPI is not always convex to the origin, so we
277
- # increment _s_2 after each iteration in which our algorithm
280
+ # increment s_2 after each iteration in which our algorithm
278
281
  # finds (s1, s2) on the boundary
279
- _s_2 = _s_2_pre * (1 + _theta)
282
+ s_2 = s_2_pre * (1 + _theta)
280
283
 
281
- if (_s_1 + _s_2) > mpf("0.99875"):
282
- # 1: # We lose accuracy at 3-9s and up
284
+ if (s_1 + s_2) > mpf("0.99875"):
285
+ # 1: # inaccuracy at 3-9s and up
283
286
  break
284
287
 
285
288
  while True:
286
- _de_1 = _s_2 / (1 - _s_1)
287
- _de_2 = (
288
- _s_1 / (1 - gbfn.lerp(_s_1, _s_2, _r_val))
289
+ de_1 = s_2 / (1 - s_1)
290
+ de_2 = (
291
+ s_1 / (1 - gbf.lerp(s_1, s_2, _r_val))
289
292
  if recapture_form == "inside-out"
290
- else _s_1 / (1 - _s_2)
293
+ else s_1 / (1 - s_2)
291
294
  )
292
295
 
293
- _weights_i = (
296
+ weights_i = (
294
297
  (
295
- _w1 := mp.fdiv(
296
- _s_2 if weighting == "cross-product-share" else _s_1,
297
- _s_1 + _s_2,
298
+ _w := mp.fdiv(
299
+ s_2 if weighting == "cross-product-share" else s_1, s_1 + s_2
298
300
  ),
299
- 1 - _w1,
301
+ 1 - _w,
300
302
  )
301
303
  if weighting
302
304
  else _weights_base
@@ -304,70 +306,226 @@ def shrratio_boundary_distance(
304
306
 
305
307
  match agg_method:
306
308
  case "arithmetic mean":
307
- _delta_test = distance_function(
308
- (_de_1, _de_2), (0.0, 0.0), p=1, w=_weights_i
309
+ delta_test = distance_function(
310
+ (de_1, de_2), (0.0, 0.0), p=1, w=weights_i
309
311
  )
310
312
  case "distance":
311
- _delta_test = distance_function(
312
- (_de_1, _de_2), (0.0, 0.0), p=2, w=_weights_i
313
+ delta_test = distance_function(
314
+ (de_1, de_2), (0.0, 0.0), p=2, w=weights_i
313
315
  )
314
316
 
315
317
  _test_flag, _incr_decr = (
316
- (_delta_test > _delta_star, -1)
318
+ (delta_test > _delta_star, -1)
317
319
  if weighting == "cross-product-share"
318
- else (_delta_test < _delta_star, 1)
320
+ else (delta_test < _delta_star, 1)
319
321
  )
320
322
 
321
323
  if _test_flag:
322
- _s_2 += _incr_decr * _gbd_step_sz
324
+ s_2 += _incr_decr * _bdry_step_sz
323
325
  else:
324
326
  break
325
327
 
326
328
  # Build-up boundary points
327
- _gbdry_points.append((_s_1, _s_2))
329
+ bdry_points.append((s_1, s_2))
328
330
 
329
331
  # Build up area terms
330
- _s_2_oddsum += _s_2 if _s_2_oddval else 0
331
- _s_2_evnsum += _s_2 if not _s_2_oddval else 0
332
- _s_2_oddval = not _s_2_oddval
332
+ s_2_oddsum += s_2 if s_2_oddval else 0
333
+ s_2_evnsum += s_2 if not s_2_oddval else 0
334
+ s_2_oddval = not s_2_oddval
333
335
 
334
336
  # Hold share points
335
- _s_2_pre = _s_2
336
- _s_1_pre = _s_1
337
+ s_2_pre = s_2
338
+ s_1_pre = s_1
337
339
 
338
- if _s_2_oddval:
339
- _s_2_evnsum -= _s_2_pre
340
+ if s_2_oddval:
341
+ s_2_evnsum -= s_2_pre
340
342
  else:
341
- _s_2_oddsum -= _s_1_pre
343
+ s_2_oddsum -= s_1_pre
342
344
 
343
- _s_intcpt = gbfn._shrratio_boundary_intcpt(
344
- _s_1_pre,
345
+ s_intcpt = gbf._shrratio_boundary_intcpt(
346
+ s_1_pre,
345
347
  _delta_star,
346
348
  _r_val,
347
349
  recapture_form=recapture_form,
348
350
  agg_method=agg_method,
349
351
  weighting=weighting,
350
352
  )
353
+ print(s_1_pre, _delta_star, _r_val, s_intcpt)
354
+ print(type(s_intcpt))
351
355
 
352
356
  if weighting == "own-share":
353
- _gbd_prtlarea = (
354
- _gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_2_pre) / 3
357
+ bdry_prtlarea = (
358
+ _bdry_step_sz * (4 * s_2_oddsum + 2 * s_2_evnsum + _s_mid + s_2_pre) / 3
355
359
  )
356
360
  # Area under boundary
357
- _gbdry_area_total = 2 * (_s_1_pre + _gbd_prtlarea) - (
358
- mp.power(_s_mid, "2") + mp.power(_s_1_pre, "2")
361
+ bdry_area_total = 2 * (s_1_pre + bdry_prtlarea) - (
362
+ mp.power(_s_mid, "2") + mp.power(s_1_pre, "2")
359
363
  )
360
364
 
361
365
  else:
362
- _gbd_prtlarea = (
363
- _gbd_step_sz * (4 * _s_2_oddsum + 2 * _s_2_evnsum + _s_mid + _s_intcpt) / 3
366
+ print([type(_f) for _f in (_s_mid, s_2_oddsum, s_2_evnsum, s_intcpt)])
367
+ bdry_prtlarea = (
368
+ _bdry_step_sz * (4 * s_2_oddsum + 2 * s_2_evnsum + _s_mid + s_intcpt) / 3
364
369
  )
365
370
  # Area under boundary
366
- _gbdry_area_total = 2 * _gbd_prtlarea - mp.power(_s_mid, "2")
371
+ bdry_area_total = 2 * bdry_prtlarea - mp.power(_s_mid, "2")
367
372
 
368
- _gbdry_points.append((mpf("0.0"), _s_intcpt))
373
+ bdry_points.append((mpf("0.0"), s_intcpt))
369
374
  # Points defining boundary to point-of-symmetry
370
- return gbfn.GuidelinesBoundary(
371
- np.vstack((_gbdry_points[::-1], np.flip(_gbdry_points[1:], 1))),
372
- round(float(_gbdry_area_total), dps),
375
+ return gbf.GuidelinesBoundary(
376
+ np.vstack((bdry_points[::-1], np.flip(bdry_points[1:], 1))),
377
+ round(float(bdry_area_total), dps),
373
378
  )
379
+
380
+
381
+ def shrratio_boundary_xact_avg_mp( # noqa: PLR0914
382
+ _delta_star: float = 0.075,
383
+ _r_val: float = DEFAULT_REC_RATIO,
384
+ /,
385
+ *,
386
+ recapture_form: Literal["inside-out", "proportional"] = "inside-out",
387
+ dps: int = 5,
388
+ ) -> gbf.GuidelinesBoundary:
389
+ """
390
+ Share combinations along the simple average diversion-ratio boundary.
391
+
392
+ Notes
393
+ -----
394
+ An analytical expression for the exact average boundary is derived
395
+ and plotted from the y-intercept to the ray of symmetry as follows::
396
+
397
+ from sympy import latex, plot as symplot, solve, symbols
398
+
399
+ s_1, s_2 = symbols("s_1 s_2")
400
+
401
+ g_val, r_val, m_val = 0.06, 0.80, 0.30
402
+ d_hat = g_val / (r_val * m_val)
403
+
404
+ # recapture_form = "inside-out"
405
+ sag = solve(
406
+ (s_2 / (1 - s_1))
407
+ + (s_1 / (1 - (r_val * s_2 + (1 - r_val) * s_1)))
408
+ - 2 * d_hat,
409
+ s_2
410
+ )[0]
411
+ symplot(
412
+ sag,
413
+ (s_1, 0., d_hat / (1 + d_hat)),
414
+ ylabel=s_2
415
+ )
416
+
417
+ # recapture_form = "proportional"
418
+ sag = solve((s_2/(1 - s_1)) + (s_1/(1 - s_2)) - 2 * d_hat, s_2)[0]
419
+ symplot(
420
+ sag,
421
+ (s_1, 0., d_hat / (1 + d_hat)),
422
+ ylabel=s_2
423
+ )
424
+
425
+ Parameters
426
+ ----------
427
+ _delta_star
428
+ Share ratio (:math:`\\overline{d} / \\overline{r}`).
429
+ _r_val
430
+ Recapture ratio
431
+ recapture_form
432
+ Whether recapture-ratio is MNL-consistent ("inside-out") or has fixed
433
+ value for both merging firms ("proportional").
434
+ dps
435
+ Number of decimal places for rounding returned shares.
436
+
437
+ Returns
438
+ -------
439
+ Array of share-pairs, area under boundary, area under boundary.
440
+
441
+ """
442
+
443
+ _delta_star = mpf(f"{_delta_star}")
444
+ _s_mid = _delta_star / (1 + _delta_star)
445
+ _bdry_step_sz = 10**-dps
446
+ _bdry_start = np.array([(_s_mid, _s_mid)])
447
+ _s_1 = np.array(mp.arange(_s_mid - _bdry_step_sz, 0, -_bdry_step_sz))
448
+ if recapture_form == "inside-out":
449
+ s_intcpt = mp.fdiv(
450
+ mp.fsub(
451
+ 2 * _delta_star * _r_val + 1, mp.fabs(2 * _delta_star * _r_val - 1)
452
+ ),
453
+ 2 * mpf(f"{_r_val}"),
454
+ )
455
+ nr_t1 = 1 + 2 * _delta_star * _r_val * (1 - _s_1) - _s_1 * (1 - _r_val)
456
+
457
+ nr_sqrt_mdr = 4 * _delta_star * _r_val
458
+ nr_sqrt_mdr2 = nr_sqrt_mdr * _r_val
459
+ nr_sqrt_md2r2 = nr_sqrt_mdr2 * _delta_star
460
+
461
+ nr_sqrt_t1 = nr_sqrt_md2r2 * (_s_1**2 - 2 * _s_1 + 1)
462
+ nr_sqrt_t2 = nr_sqrt_mdr2 * _s_1 * (_s_1 - 1)
463
+ nr_sqrt_t3 = nr_sqrt_mdr * (2 * _s_1 - _s_1**2 - 1)
464
+ nr_sqrt_t4 = (_s_1**2) * (_r_val**2 - 6 * _r_val + 1)
465
+ nr_sqrt_t5 = _s_1 * (6 * _r_val - 2) + 1
466
+
467
+ nr_t2_mdr = nr_sqrt_t1 + nr_sqrt_t2 + nr_sqrt_t3 + nr_sqrt_t4 + nr_sqrt_t5
468
+
469
+ # Alternative grouping of terms in np.sqrt
470
+ nr_sqrt_s1sq = (_s_1**2) * (
471
+ nr_sqrt_md2r2 + nr_sqrt_mdr2 - nr_sqrt_mdr + _r_val**2 - 6 * _r_val + 1
472
+ )
473
+ nr_sqrt_s1 = _s_1 * (
474
+ -2 * nr_sqrt_md2r2 - nr_sqrt_mdr2 + 2 * nr_sqrt_mdr + 6 * _r_val - 2
475
+ )
476
+ nr_sqrt_nos1 = nr_sqrt_md2r2 - nr_sqrt_mdr + 1
477
+
478
+ nr_t2_s1 = nr_sqrt_s1sq + nr_sqrt_s1 + nr_sqrt_nos1
479
+
480
+ if not np.isclose(
481
+ np.einsum("i->", nr_t2_mdr.astype(float)),
482
+ np.einsum("i->", nr_t2_s1.astype(float)),
483
+ rtol=0,
484
+ atol=0.5 * dps,
485
+ ):
486
+ raise RuntimeError(
487
+ "Calculation of sq. root term in exact average GUPPI"
488
+ f"with recapture spec, {f'"{recapture_form}"'} is incorrect."
489
+ )
490
+
491
+ s_2 = (nr_t1 - np.sqrt(nr_t2_s1)) / (2 * _r_val)
492
+
493
+ else:
494
+ s_intcpt = mp.fsub(_delta_star + 1 / 2, mp.fabs(_delta_star - 1 / 2))
495
+ s_2 = (
496
+ (1 / 2)
497
+ + _delta_star
498
+ - _delta_star * _s_1
499
+ - np.sqrt(
500
+ ((_delta_star**2) - 1) * (_s_1**2)
501
+ + (-2 * (_delta_star**2) + _delta_star + 1) * _s_1
502
+ + (_delta_star**2)
503
+ - _delta_star
504
+ + (1 / 4)
505
+ )
506
+ )
507
+
508
+ bdry_inner = np.column_stack((_s_1, s_2))
509
+ bdry_end = np.array([(mpf("0.0"), s_intcpt)])
510
+
511
+ bdry = np.vstack((
512
+ bdry_end,
513
+ bdry_inner[::-1],
514
+ _bdry_start,
515
+ np.flip(bdry_inner, 1),
516
+ np.flip(bdry_end, 1),
517
+ ))
518
+ s_2 = np.concatenate((np.array([_s_mid]), s_2))
519
+
520
+ bdry_ends = [0, -1]
521
+ bdry_odds = np.array(range(1, len(s_2), 2), int)
522
+ bdry_evns = np.array(range(2, len(s_2), 2), int)
523
+
524
+ # Double the area under the half-curve, and subtract the double-counted bit.
525
+ bdry_area_simpson = 2 * _bdry_step_sz * (
526
+ (4 / 3) * np.sum(s_2.take(bdry_odds))
527
+ + (2 / 3) * np.sum(s_2.take(bdry_evns))
528
+ + (1 / 3) * np.sum(s_2.take(bdry_ends))
529
+ ) - mp.power(_s_mid, 2)
530
+
531
+ return gbf.GuidelinesBoundary(bdry, float(mp.nstr(bdry_area_simpson, dps)))