image_2layer — Tagg / Sunde series for 2-layer soil¶
Physical context¶
The 2-layer model is the canonical typical layered-soil description: a finite upper layer of thickness \(h_1\) and resistivity \(\rho_1\) over a semi-infinite lower layer of resistivity \(\rho_2\). It is the simplest soil that captures the dominant first-order effect of heterogeneity — a frozen, weathered, or saturated surface layer above a different sub-stratum — without introducing the identification problem that more layers create. A typical parameter range is \(\rho_1 \in [50, 1000]\,\Omega\, \text{m}\), \(\rho_2 \in [10, 5000]\,\Omega\,\text{m}\), \(h_1 \in [0.5, 5]\,\text{m}\).
The single design degree of freedom that controls the layered behaviour is the interface reflection coefficient
For \(\rho_2 < \rho_1\) (\(K_1 < 0\), conductive bottom) the lower layer pulls current away from the surface and the cluster impedance falls. For \(\rho_2 > \rho_1\) (\(K_1 > 0\), resistive bottom) the opposite holds. The limiting cases \(K_1 \to \pm 1\) correspond to a perfect insulator below or a perfect conductor below.
Governing equation: Tagg / Sunde geometric series¶
A point current source \(I\) at depth \(z_s \in (0, h_1)\) produces a potential field that satisfies the Neumann boundary at \(z = 0\) and the continuity-of-current boundary at \(z = h_1\). The spectral-domain solution involves the Sommerfeld integral
Because \(\Gamma_1(\lambda) \equiv K_1\) is constant in \(\lambda\) (only one interface contributes) the multiple-reflection multiplier \(1 / (1 - K_1 e^{-2\lambda h_1})\) expands as a geometric series in \(K_1 e^{-2\lambda h_1}\), and the Sommerfeld integral collapses into a closed-form sum of point sources at mirrored positions.
After substitution and simplification, the spatial form is the classical Tagg / Sunde series:
with the image distances
Per index \(n\) four images are placed at \(z_n^{\sigma\tau} = \sigma\,2 n h_1 + \tau\,z_s\) with weight \(K_1^n\). The \(n=0\) term contributes two images (the original source at \(z = +z_s\) and the air mirror at \(z = -z_s\), both with weight 1) — this is exactly the homogeneous image-charge solution.
Numerical strategy¶
Series truncation¶
The series is truncated at the smallest index for which the geometric tail bound falls below the tolerance,
or at max_terms (Engine.image_max_terms, default \(300\)),
whichever happens first. The tail bound is the exact geometric
remainder of the weight series \(\sum_{m>n} |K_1|^m\); the historic
per-term test \(|K_1|^n < \text{tol}\) underestimated the neglected
remainder by the factor \(1/(1-|K_1|)\) — a factor 17 at
\(|K_1| = 0.94\). The default max_terms covers \(|K_1| = 0.94\)
(277 terms); harder contrasts converge more slowly and the
per-segment cost grows linearly with the required number of image
terms.
When max_terms is reached without meeting the tolerance,
FieldResult.metadata['converged'] is set to False, a warning is
logged and a SeriesTruncationWarning is emitted (silence with
warnings.simplefilter("ignore", SeriesTruncationWarning) or raise
Engine.image_max_terms).
Self-action¶
The wire-segment self-action splits cleanly:
- The \(n = 0\) direct contribution carries the singular line self-potential — handled by the homogeneous formula \(2 \ln(L_i / a_i) / L_i\).
- The \(n = 0\) air-mirror term and all \(n \ge 1\) image terms are at least \(2 z_s\) (or \(2 h_1 - z - z_s\), etc.) away from the segment midpoint, so the point-source approximation is safe.
This is the same construction as in the homogeneous backend, with \(\rho \to \rho_1\) as the prefactor and the geometric image series added on top.
Auto-dispatch¶
Engine.solve picks image_2layer automatically when the user
passed backend="image" and world.soil is a TwoLayerSoil. A
client code written for the homogeneous backend therefore keeps
working when only the soil model is swapped — no string change
required.
Validity envelope¶
| Property | Range / value |
|---|---|
| Soil model | TwoLayerSoil only |
| Frequency | quasi-static, \(f < 1\,\text{kHz}\) |
| Electrode placement | every segment must have \(z < h_1\) (raises ValueError otherwise) |
| Reflection coefficient | $ |
| Series truncation | adaptive tail bound $ |
| Wire radius / segment ratio | thin-wire, \(a \ll L_i\) |
The hard constraint that all segments lie inside the upper layer is a direct consequence of the series construction: the image positions \(\pm 2 n h_1 \pm z_s\) are derived assuming the source is above the interface. A segment at \(z = h_1\) would put a source on the interface and break the geometric expansion.
Convergence and cost¶
- Series convergence. Geometric in \(|K_1|\) (terms for the \(10^{-6}\) tail bound):
- \(|K_1| = 0.5\) → 20 terms.
- \(|K_1| = 0.8\) → 69 terms.
- \(|K_1| = 0.9\) → 153 terms.
- \(|K_1| = 0.94\) → 277 terms (just inside the default
max_terms = 300). - \(|K_1| = 0.95\) → 328 terms (above the default — raises a
SeriesTruncationWarning; increaseEngine.image_max_terms). - Per-segment cost. \(O(N^2 \cdot M)\) kernel evaluations with \(M\) the truncated series length. For typical cases with \(N \le 1000\) and \(M \le 200\) the engine still runs in seconds.
- Reduction to homogeneous. At \(K_1 = 0\) the engine collapses
to the homogeneous backend bit-exactly — the only series term
with non-zero weight is \(n = 0\) with weight \(1\), identical to the
imagebackend with \(\rho = \rho_1\).
Cross-validation notes¶
| Counterpart | Expected agreement | What is checked |
|---|---|---|
image at \(K_1 = 0\) |
bit-exact | series collapse |
mom on the same world |
\(\le 2\,\%\) | same kernel, different resolution scheme |
cim on the same world |
bit-exact (CIM falls back on this kernel for \(n = 2\)) | cim deliberately reuses the Tagg / Sunde self-kernel for \(n = 2\) |
bem on the same world |
bit-exact (BEM falls back on this kernel for \(n = 2\)) | same reason |
mom_sommerfeld on the same world |
\(\le 5\,\%\) | quadrature reproduces the geometric series |
| Sunde / Dwight closed forms | \(\le 10\,\%\) | reference values for canonical geometries (rods, rings) |
The engine's role inside the cross-validation matrix is to be the closed-form anchor: every other engine is checked against it for \(n = 2\).
References¶
- Tagg, G. F. (1964). Earth Resistances, Pitman, ch. 5. The classical statement of the geometric image series.
- Sunde, E. D. (1968). Earth Conduction Effects in Transmission Systems, Dover, sect. 3.5. Derivation of the multi-layer Sommerfeld integral and the 2-layer reduction.
- Stefanescu, S. & Schlumberger, C. (1930). Sur la distribution électrique potentielle autour d'une prise de terre ponctuelle, Journal de Physique. Original geophysical formulation of the image-charge series.
- Dawalibi, F. P. & Barbeito, N. (1991). Measurements and computations of the performance of grounding systems buried in multilayer soils. IEEE PWRD 6(4) — extension to \(n\) layers and modern computational practice.
- Colominas, I., París, J., Navarrina, F. & Casteleiro, M. (2012). Improvement of computer methods for grounding analysis in layered soils by using high-efficient convergence acceleration techniques. Adv. Eng. Soft. 44 — Aitken / Pade acceleration of the series for \(|K_1| \to 1\).
Example¶
import groundfield as gf
soil = gf.TwoLayerSoil(rho_1=100.0, rho_2=500.0, h_1=2.0)
world = gf.create_world(soil=soil)
gf.create_electrode(world, "ring", name="g1",
center=(0.0, 0.0, 0.8), radius=5.0,
wire_radius=0.005)
gf.create_source(world, attached_to="g1", magnitude=1.0)
engine = gf.create_engine(backend="image_2layer",
segment_length=0.1,
frequencies=[50.0])
result = world.solve(engine)
print(result.cluster_impedance("g1")[0])
API reference¶
image_2layer ¶
Image-charge backend for 2-layer soil (Tagg / Sunde).
Computes the potential field in a layered half-space (upper layer \(\rho_1\) of thickness \(h_1\), semi-infinite lower layer \(\rho_2\)) using the classical image-charge series.
Theory (short)
With the reflection coefficient at the layer interface $$ K \;=\; \frac{\rho_2 - \rho_1}{\rho_2 + \rho_1} $$ a point current source \(I\) at \((x_s, y_s, z_s)\) with \(0 < z_s < h_1\) produces, at any field point in the upper layer, the potential $$ \varphi(x, y, z) \;=\; \frac{\rho_1\, I}{4\pi}\,\Bigl[ \tfrac{1}{r_0^+} + \tfrac{1}{r_0^-} + \sum_{n=1}^{\infty} K^n \Bigl(\tfrac{1}{r_n^{++}} + \tfrac{1}{r_n^{+-}} + \tfrac{1}{r_n^{-+}} + \tfrac{1}{r_n^{--}}\Bigr)\Bigr], $$ with the image-source distances $$ r_n^{\sigma\tau} \;=\; \sqrt{(x-x_s)^2 + (y-y_s)^2 + (z - \sigma\,2 n h_1 - \tau z_s)^2}, \qquad \sigma, \tau \in {+1,-1}. $$ The \(n = 0\) term (two images, weight 1) is exactly the homogeneous image-charge backend. The 2-layer backend therefore reduces to the homogeneous result for \(\rho_2 = \rho_1\) (i.e. \(K = 0\)) — usable as a consistency test.
Convergence
Series terms decay as \(K^n\) (geometric). For \(|K| < 1\),
typically \(n \lesssim 50\) is enough for a relative accuracy of
\(10^{-6}\). The implementation truncates after max_terms
terms or as soon as \(|K|^n < \text{tol}\). If max_terms is
hit without reaching the tolerance, a warning is recorded in
FieldResult.metadata.
Preconditions
- All electrodes must lie completely in the upper layer
(\(z_\text{seg} < h_1\)); otherwise the backend raises a clear
ValueError. - For \(|K| \to 1\) (extreme contrasts, e.g. wet soil over rock) convergence slows down significantly. The solver issues a log warning in that case.
References
- Tagg, G. F. (1964). Earth Resistances. Pitman, ch. 5.
- Sunde, E. D. (1968). Earth Conduction Effects in Transmission Systems. Dover, sect. 3.5.
- ADR-0001
docs/adr/0001-two-layer-method.md.
SeriesTruncationWarning ¶
Bases: UserWarning
The Tagg/Sunde image series hit max_terms before tol.
Emitted (in addition to the logger record) whenever a backend
truncates the two-layer image-charge series without reaching the
requested tail tolerance — typically at high layer contrast
(:math:|K| \to 1). Silence with
warnings.simplefilter("ignore", SeriesTruncationWarning) or
raise Engine.image_max_terms.
solve_image_2layer ¶
solve_image_2layer(
world: "World",
engine: "Engine",
*,
max_terms: int = 100,
tol: float = 1e-06
) -> FieldResult
Image-charge solver for 2-layer soil (Tagg / Sunde).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
world
|
'World'
|
World whose |
required |
engine
|
'Engine'
|
Engine configuration. |
required |
max_terms
|
int
|
Maximum number of series terms. |
100
|
tol
|
float
|
Series truncation: stop as soon as \(|K|^n < \text{tol}\). |
1e-06
|
Returns:
| Type | Description |
|---|---|
FieldResult
|
Result object. :attr: |
Source code in src/groundfield/solver/image_2layer.py
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 | |
Related material¶
- ADR-0001 — original methodology decision.