Translating Echoes scripts: echoes2mfh
From Echoes to MeanFieldHomogenization explains the correspondence between the two APIs. tools/echoes2mfh/ automates it: point it at an Echoes Python script and it writes the Julia one.
It is deterministic — the same input always gives the same output — and it is plain Python 3.10+ with no dependencies beyond the standard library, so it runs without Julia or Echoes installed.
cd tools/echoes2mfh
python3 -m echoes2mfh survey /path/to/echoes_cpp/tests/python
python3 -m echoes2mfh translate porous.py -o 90_porous.jl --report porous.json
python3 -m echoes2mfh check-drift -vWhat it does to a script
Take the classic porous benchmark, echoes_tests/porous.py:
ver = rve(matrix="SOLID")
ver["SOLID"] = ellipsoid(shape=spheroidal(1.), symmetrize=[ISO], prop={"C": Cs})
ver["PORE"] = ellipsoid(shape=spheroidal(1.), symmetrize=[ISO], prop={"C": Cp})
def Chom(f, sch):
ver["PORE"].fraction = f ; ver["SOLID"].fraction = 1 - f
C = homogenize(prop="C", rve=ver, scheme=sch,
maxnb=300, epsrel=1.e-10, select_best=True)
return max(C.k, 0.), max(C.mu, 0.)and the translation:
function build_ver(f)
rve = RVE()
add_phase!(rve, :SOLID, Spheroid(1.0), Dict(:C => Cs); fraction = :rest, symmetrize = IsoSymmetrize())
add_phase!(
rve, :PORE, Spheroid(1.0), Dict(:C => Cp);
fraction = f, symmetrize = IsoSymmetrize()
)
return rve
end
function Chom(f, sch)
# `SOLID` is the matrix: MFH derives its fraction as 1 - Σ f_inclusions
C = homogenize(build_ver(f), sch, :C)
return (max(k_mu(C)[1], 0.0), max(k_mu(C)[2], 0.0))
endThree things happened that are not local rewrites:
The RVE was lifted into a builder. Echoes mutates one module-level RVE; MFH's RVE is immutable, and its matrix fraction is derived (1 - Σ f) rather than settable. So the construction becomes a function of whatever the script varies. Note that ver["SOLID"].fraction = 1 - f is dropped, not translated — in MFH it is not merely redundant, it raises.
Solver options moved into the scheme. Echoes passes epsrel, maxnb and select_best as loose homogenize keywords; MFH attaches them to the scheme instance, so scheme=SC, epsrel=1e-10, maxnb=300, select_best=True becomes SelfConsistent(; reltol = 1.0e-10, abstol = 0.0, maxiters = 300, select_best = true). Keywords a given scheme does not read are dropped rather than passed on.
epsrel becomes reltol, and it comes with abstol = 0.0. Echoes' fixed point stopped on ‖X - X_old‖ > epsrel · ‖X_old‖, with no absolute term; MFH's test is the additive abstol + reltol · ‖x‖, so switching the absolute term off is what reproduces the original stop exactly. Leaving it at its 1e-12 default would make a tight request such as epsrel = 1e-15 stop earlier than Echoes did, on the absolute term instead of the relative one.
Accumulate-in-a-loop became a comprehension. k = [] followed by for x in xs: k.append(f(x)) becomes k = [f(x) for x in xs], and when several accumulators read components of one call, that call is evaluated once per step instead of once per accumulator.
Refusals
Anything the translator cannot render faithfully becomes a block carrying the original Python, plus a runtime error(...):
#= UNTRANSLATED line 26 severity=blocking
iteration over an RVE (`for x in ver` / `x.data()`)
-> MFH exposes the phases by name: iterate inclusion_phase_names(rve)
-> and use volume_fraction(rve, name) plus the localization functions
Original Python:
moyA = sum([x.data().factor * isotropify(x.data().eE) for x in ver], 0)
=#
error("echoes2mfh: untranslated construct at line 26 — iteration over an RVE")A partially translated script therefore cannot quietly produce wrong numbers. This is deliberate: a plausible-looking wrong result ends up in a paper, while a refusal costs an afternoon of hand-porting.
Exit codes are 0 (clean), 2 (translated, with findings) and 3 (refused — typically Python 2 syntax, which 2to3 fixes first).
Tensor conventions
tensor(...) is the one place where reading the Echoes C++ sources was unavoidable, because the parameter-list length selects both the order and the symmetry class, and the class docstrings do not describe what the generic builder actually calls:
| params | meaning | MFH / TensND |
|---|---|---|
| 2 | isotropic 4th order, | TensISO{3}(α, β) |
| 3 (+3 angles) | 2nd order, eigenvalues in the rotated frame | Tens(diag, Basis(P)) |
| 5 (+2 angles) | transversely isotropic, sym-Walpole coefficients | TensTI{4,T,5}(p, n) |
| 9 (+3 angles) | orthotropic, Cijkl components | inv_KM(M, Basis(P)) |
| 21 | triclinic | refused — component ordering unverified |
Two traps are worth stating. The 5-parameter form is not the tensor_ti::build, which reads coefficients on the sym-Walpole basis, while those components belong to build_Cijkl, which it never calls. The 9-parameter form, by contrast, is the Cijkl one — the two arities genuinely disagree.
Echoes' sym-Walpole basis turns out to be TensND's Walpole basis with N = 5 layout, so the five coefficients map across in order. The correspondence was checked numerically to
The complex twins (rvec, ellipsoidc, tensorc, …) exist only because the C++ templates are instantiated separately for real and complex scalars. Julia's constructors are generic over the scalar type, so the suffix drops and the RVE is declared RVE(; T = ComplexF64).
Coverage
Over the 225 scripts of echoes_tests/, creep/, echoes_concrete/ and spheroid_nlayers/:
| scripts | |
|---|---|
| parse as Python 3 | 214 |
Python 2 syntax (refused; run 2to3 first) | 11 |
| generated Julia that parses | 214 / 214 |
| translate with no findings at all | 36 |
| translate with 1–3 findings | 46 |
| translate with more findings | 132 |
The finding counts are occurrences rather than files, and they are a work queue rather than a verdict: survey ranks the unmapped symbols by how many scripts each one blocks. echoes_tests/porous.py translates, runs, and reproduces the captured Echoes 1.0 values exactly (Mori-Tanaka at
Staying in step
check-drift compares the mapping tables against both live APIs: it scrapes the Echoes symbol list from the pybind11 sources and the MFH export list from src/MeanFieldHomogenization.jl, then reports any symbol that is neither mapped nor explicitly refused, and any mapping target MFH no longer exports.
See also
From Echoes to MeanFieldHomogenization — the API correspondence the tool automates.
Cross-validation against Echoes — the deliberate divergences, including the crack-compliance normalization (
, ), which the translator never applies silently.MFH Studio — the graphical builder. It shares this tool's code generator, so both write the same style of Julia, and its Open runs this translator when handed a
.py.