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