Skip to content

Commit e89c30b

Browse files
authored
Merge pull request #653 from bobmyhill/fix_l2_plus_tests
Fix l2 plus tests
2 parents a0d43ec + 200ac5a commit e89c30b

File tree

11 files changed

+326
-111
lines changed

11 files changed

+326
-111
lines changed

burnman/utils/math.py

Lines changed: 67 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,6 @@ def unit_normalize(a, order=2, axis=-1):
3030
return a / np.expand_dims(l2, axis)[0][0]
3131

3232

33-
def float_eq(a, b):
34-
"""
35-
Test if two floats are almost equal to each other
36-
"""
37-
return abs(a - b) < 1e-10 * max(1e-5, abs(a), abs(b))
38-
39-
4033
def linear_interpol(x, x1, x2, y1, y2):
4134
"""
4235
Linearly interpolate to point x, between
@@ -353,7 +346,7 @@ def interp_smoothed_array_and_derivatives(
353346
return interps
354347

355348

356-
def compare_l2(depth, calc, obs):
349+
def l2_norm_profiles(depth, calc, obs):
357350
"""
358351
Computes the L2 norm for N profiles at a time (assumed to be linear between points).
359352
@@ -369,91 +362,85 @@ def compare_l2(depth, calc, obs):
369362
"""
370363
err = []
371364
for i in range(len(calc)):
372-
err.append(l2(depth, calc[i], obs[i]))
365+
err.append(l2_norm_profile(depth, calc[i], obs[i]))
373366

374367
return err
375368

376369

377-
def compare_chifactor(calc, obs):
370+
def chisqr_profiles(calc, obs):
378371
"""
379-
Computes the chi factor for N profiles at a time. Assumes a 1% a priori uncertainty on the seismic model.
380-
372+
Computes the chisqr factor for N profiles at a time. Assumes a 1% a priori uncertainty on the seismic model.
381373
382374
:type calc: list of arrays of float
383375
:param calc: N arrays calculated values, e.g. [mat_vs,mat_vphi]
384376
:type obs: list of arrays of float
385-
:param obs: N arrays of values (observed or calculated) to compare to , e.g. [seis_vs, seis_vphi]
377+
:param obs: N arrays of values (observed or calculated) to compare to, e.g. [seis_vs, seis_vphi]
386378
387379
:returns: error array of length N
388380
:rtype: array of floats
389381
"""
390382
err = []
391383
for i in range(len(calc)):
392-
err.append(chi_factor(calc[i], obs[i]))
384+
err.append(chisqr_profile(calc[i], obs[i]))
393385

394-
return err
386+
return np.array(err)
395387

396388

397-
def l2(x, funca, funcb):
389+
def l2_norm_profile(x, funca, funcb):
398390
"""
399-
Computes the L2 norm for one profile(assumed to be linear between points).
391+
Computes the L2 norm of the difference between two 1D profiles,
392+
assuming linear interpolation between points.
400393
401-
:type x: array of float
402-
:param x: depths :math:`[m]`.
403-
:type funca: list of arrays of float
404-
:param funca: array calculated values
405-
:type funcb: list of arrays of float
406-
:param funcb: array of values (observed or calculated) to compare to
394+
This is equivalent to the square root of the integrated squared
395+
difference between the two functions over the given depth range.
396+
The integration is performed using the trapezoidal rule.
407397
408-
:returns: L2 norm
409-
:rtype: array of floats
410-
"""
411-
diff = np.array(funca - funcb)
412-
diff = diff * diff
398+
:param x: Array of depth values [m], must be 1D and monotonically increasing.
399+
:type x: array-like of float
413400
414-
return integrate.trapezoid(diff, x)
401+
:param funca: First profile (e.g., model or predicted values), must be same length as x.
402+
:type funca: array-like of float
415403
404+
:param funcb: Second profile (e.g., observed or reference values), same shape as funca.
405+
:type funcb: array-like of float
416406
417-
def nrmse(x, funca, funcb):
407+
:return: L2 norm (scalar)
408+
:rtype: float
418409
"""
419-
Normalized root mean square error for one profile
420-
:type x: array of float
421-
:param x: depths in m.
422-
:type funca: list of arrays of float
423-
:param funca: array calculated values
424-
:type funcb: list of arrays of float
425-
:param funcb: array of values (observed or calculated) to compare to
426-
427-
:returns: RMS error
428-
:rtype: array of floats
410+
diff = np.square(funca - funcb)
411+
return np.sqrt(integrate.trapezoid(diff, x))
412+
413+
414+
def chisqr_profile(calc, obs, uncertainties=0.01):
429415
"""
430-
diff = np.array(funca - funcb)
431-
diff = diff * diff
432-
rmse = np.sqrt(np.sum(diff) / x)
433-
nrmse = rmse / (np.max(funca) - np.min(funca))
416+
Computes the :math:`\\chi^2` factor for a single profile, assuming a 1 %
417+
uncertainty on the reference (observed) values. This provides a normalized
418+
measure of the deviation between calculated and reference values.
434419
435-
return nrmse
420+
The :math:`\\chi^2` factor is calculated as:
436421
422+
.. math::
437423
438-
def chi_factor(calc, obs):
439-
"""
440-
:math:`\\chi` factor for one profile assuming 1% uncertainty on the reference model (obs)
441-
:type calc: list of arrays of float
442-
:param calc: array calculated values
443-
:type obs: list of arrays of float
444-
:param obs: array of reference values to compare to
424+
\\chi^2 = \\frac{1}{N} \\sum_{i=1}^{N}
425+
\\left( \\frac{\\text{calc}_i - \\text{obs}_i}{0.01 \\cdot \\text{mean}(\\text{obs})} \\right)^2
445426
446-
:returns: :math:`\\chi` factor
447-
:rtype: array of floats
448-
"""
427+
where :math:`N` is the number of elements in the profile.
449428
450-
err = np.empty_like(calc)
451-
for i in range(len(calc)):
452-
err[i] = pow((calc[i] - obs[i]) / (0.01 * np.mean(obs)), 2.0)
429+
:param calc: Array of calculated values (e.g., from a model).
430+
:type calc: array
453431
454-
err_tot = np.sum(err) / len(err)
432+
:param obs: Array of reference or observed values.
433+
:type obs: array
434+
435+
:param uncertainties: Array of uncertainties values.
436+
:type uncertainties: array or float
437+
438+
:return: The :math:`\\chi^2` factor for the profile.
439+
:rtype: float
440+
"""
455441

456-
return err_tot
442+
err = np.square((calc - obs) / (uncertainties * np.mean(obs)))
443+
return np.sum(err) / len(err)
457444

458445

459446
def independent_row_indices(array):
@@ -495,27 +482,33 @@ def array_to_rational_matrix(array):
495482

496483
def complete_basis(basis, flip_columns=True):
497484
"""
498-
Creates a full basis by filling remaining rows with
499-
selected rows of the identity matrix that have indices
500-
not in the column pivot list of the basis RREF
501-
502-
:param basis: A partial basis
503-
:type basis: numpy array
504-
:param flip_columns: whether to calculate the RREF
505-
from a flipped basis. This can be useful, for example,
506-
when dependent columns are known to be further to the
507-
right in the basis. defaults to True
485+
Extends a partial basis (with fewer rows than columns) to a full basis
486+
by adding rows from the identity matrix that are linearly independent
487+
of the existing rows.
488+
489+
This is done by computing the reduced row echelon form (RREF) of the basis
490+
to identify pivot columns. Identity rows corresponding to non-pivot columns
491+
are added to complete the basis.
492+
493+
:param basis: A 2D NumPy array representing a partial basis, with shape (n, m)
494+
where n < m.
495+
:type basis: numpy.ndarray
496+
497+
:param flip_columns: If True, the basis columns are flipped (reversed) before
498+
computing the RREF. This is helpful if the dependent columns
499+
are expected to appear later in the matrix. Default is True.
508500
:type flip_columns: bool, optional
509-
:return: basis
510-
:rtype: numpy array
511-
"""
512501
502+
:return: A completed basis as a NumPy array with shape (m, m), containing the
503+
original rows plus additional rows from the identity matrix.
504+
:rtype: numpy.ndarray
505+
"""
513506
n, m = basis.shape
514507
if n < m:
515508
if flip_columns:
516509
pivots = list(m - 1 - np.array(Matrix(np.flip(basis, axis=1)).rref()[1]))
517510
else:
518-
pivots = list(Matrix(basis)).rref()[1]
511+
pivots = list(Matrix(basis).rref())[1]
519512

520513
return np.concatenate(
521514
(basis, np.identity(m)[[i for i in range(m) if i not in pivots], :]), axis=0

contrib/CHRU2014/paper_onefit.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -324,14 +324,16 @@ def array_to_rock(arr, names):
324324
["rho", "v_p", "v_s", "v_phi", "K_S", "G"], pressure, temperature
325325
)
326326

327-
err_vs, err_vphi, err_rho = burnman.utils.math.compare_l2(
328-
depths / np.mean(depths),
329-
[vs / np.mean(seis_vs), vphi / np.mean(seis_vphi), rho / np.mean(seis_rho)],
330-
[
331-
seis_vs / np.mean(seis_vs),
332-
seis_vphi / np.mean(seis_vphi),
333-
seis_rho / np.mean(seis_rho),
334-
],
327+
err_vs, err_vphi, err_rho = np.square(
328+
burnman.utils.math.l2_norm_profiles(
329+
depths / np.mean(depths),
330+
[vs / np.mean(seis_vs), vphi / np.mean(seis_vphi), rho / np.mean(seis_rho)],
331+
[
332+
seis_vs / np.mean(seis_vs),
333+
seis_vphi / np.mean(seis_vphi),
334+
seis_rho / np.mean(seis_rho),
335+
],
336+
)
335337
)
336338
error = np.sum([err_rho, err_vphi, err_vs])
337339

contrib/CHRU2014/paper_opt_pv.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ def eval_material(amount_perovskite):
9797
mat_rho, mat_vs, mat_vphi = rock.evaluate(
9898
["rho", "v_s", "v_phi"], seis_p, temperature
9999
)
100-
# [rho_err,vphi_err,vs_err]=burnman.utils.compare_chifactor(mat_vs,mat_vphi,mat_rho,seis_vs,seis_vphi,seis_rho)
101-
102100
return seis_p, mat_vs, mat_vphi, mat_rho
103101

104102
def material_error(x):
105103
_, mat_vs, mat_vphi, mat_rho = eval_material(x)
106-
[vs_err, vphi_err, rho_err] = burnman.utils.math.compare_l2(
107-
depths, [mat_vs, mat_vphi, mat_rho], [seis_vs, seis_vphi, seis_rho]
104+
[vs_err, vphi_err, rho_err] = np.square(
105+
burnman.utils.math.l2_norm_profiles(
106+
depths, [mat_vs, mat_vphi, mat_rho], [seis_vs, seis_vphi, seis_rho]
107+
)
108108
)
109109
scale = 2700e3 - 850e3
110110
return vs_err / scale, vphi_err / scale

contrib/cider_tutorial_2014/step_2.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ def misfit(phase_1_fraction):
8787

8888
# Here we integrate an L2 difference with depth between our calculated seismic
8989
# profiles and PREM. We then return those misfits.
90-
[vs_err, vphi_err, rho_err] = burnman.utils.math.compare_l2(
91-
depths, [vs, vphi, density], [seis_vs, seis_vphi, seis_rho]
90+
[vs_err, vphi_err, rho_err] = np.square(
91+
burnman.utils.math.l2_norm_profiles(
92+
depths, [vs, vphi, density], [seis_vs, seis_vphi, seis_rho]
93+
)
9294
)
9395

9496
return vs_err, vphi_err, rho_err

examples/example_composite_seismic_velocities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@
133133
["density", "v_p", "v_phi", "v_s", "K_S", "G"], seis_p, temperature
134134
)
135135

136-
[vs_err, vphi_err, rho_err] = burnman.utils.math.compare_chifactor(
136+
[vs_err, vphi_err, rho_err] = burnman.utils.math.chisqr_profiles(
137137
[mat_vs, mat_vphi, mat_rho], [seis_vs, seis_vphi, seis_rho]
138138
)
139139

examples/example_inv_murakami.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import burnman
1111
from burnman import minerals
12+
from burnman.utils.math import l2_norm_profile
1213

1314
import pymc
1415
import cProfile
@@ -43,12 +44,9 @@ def calc_velocities(amount_pv, iron_pv, iron_fp):
4344
def error(amount_pv, iron_pv, iron_fp):
4445
mat_vp, mat_vs, mat_rho = calc_velocities(amount_pv, iron_pv, iron_fp)
4546

46-
vs_err = burnman.utils.math.l2(depths, mat_vs, seis_vs) / 1e9
47-
vp_err = burnman.utils.math.l2(depths, mat_vp, seis_vp) / 1e9
48-
den_err = burnman.utils.math.l2(depths, mat_rho, seis_rho) / 1e9
49-
50-
# print vs_err, vp_err, den_err
51-
47+
vs_err = l2_norm_profile(depths, mat_vs, seis_vs) / 1.0e3
48+
vp_err = l2_norm_profile(depths, mat_vp, seis_vp) / 1.0e3
49+
den_err = l2_norm_profile(depths, mat_rho, seis_rho) / 1.0e3
5250
return vs_err + vp_err + den_err
5351

5452
# Priors on unknown parameters:
@@ -282,7 +280,7 @@ def theta(amount_pv=amount_pv, iron_pv=iron_pv, iron_fp=iron_fp):
282280
markersize=4,
283281
label="ref",
284282
)
285-
plt.title("density ($\cdot 10^3$ kg/m^$3$)")
283+
plt.title("density ($\\cdot 10^3$ kg/m^$3$)")
286284
plt.legend(loc="upper left")
287285
plt.ylim([4, 8])
288286
plt.savefig("output_figures/example_inv_murakami_2.png")
@@ -525,7 +523,7 @@ def theta(amount_pv=amount_pv, iron_pv=iron_pv, iron_fp=iron_fp):
525523
markersize=4,
526524
label="ref",
527525
)
528-
plt.title("density ($\cdot 10^3$ kg/m$^3$)")
526+
plt.title("density ($\\cdot 10^3$ kg/m$^3$)")
529527
plt.legend(loc="upper left")
530528
plt.ylim([4, 8])
531529
plt.savefig("output_figures/example_inv_murakami_2.png")

examples/example_optimize_pv.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* :class:`burnman.seismic.PREM`
1919
* :func:`burnman.geotherm.brown_shankland`
2020
* :func:`burnman.Material.evaluate`
21-
* :func:`burnman.utils.math.compare_l2`
21+
* :func:`burnman.utils.math.l2_norm_profiles`
2222
2323
*Demonstrates:*
2424
@@ -76,10 +76,12 @@ def material_error(amount_perovskite):
7676
print("Calculations are done for:")
7777
rock.debug_print()
7878
# Calculate errors
79-
[vs_err, vphi_err, rho_err, K_err, G_err] = burnman.utils.math.compare_l2(
80-
depths,
81-
[mat_vs, mat_vphi, mat_rho, mat_K, mat_G],
82-
[seis_vs, seis_vphi, seis_rho, seis_K, seis_G],
79+
[vs_err, vphi_err, rho_err, K_err, G_err] = np.square(
80+
burnman.utils.math.l2_norm_profiles(
81+
depths,
82+
[mat_vs, mat_vphi, mat_rho, mat_K, mat_G],
83+
[seis_vs, seis_vphi, seis_rho, seis_K, seis_G],
84+
)
8385
)
8486
# Normalize errors
8587
vs_err = vs_err / np.mean(seis_vs) ** 2.0

examples/example_user_input_material.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def __init__(self):
112112
["density", "v_s", "v_phi"], seis_p, temperature
113113
)
114114

115-
[vs_err, vphi_err, rho_err] = burnman.utils.math.compare_chifactor(
115+
[vs_err, vphi_err, rho_err] = burnman.utils.math.chisqr_profiles(
116116
[mat_vs, mat_vphi, mat_rho], [seis_vs, seis_vphi, seis_rho]
117117
)
118118

misc/performance.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
# This file is part of BurnMan - a thermoelastic and thermodynamic toolkit for the Earth and Planetary Sciences
2-
# Copyright (C) 2012 - 2015 by the BurnMan team, released under the GNU
1+
# This file is part of BurnMan - a thermoelastic and thermodynamic toolkit
2+
# for the Earth and Planetary Sciences
3+
# Copyright (C) 2012 - 2025 by the BurnMan team, released under the GNU
34
# GPL v2 or later.
45

56
import numpy as np
67

78
import burnman
89
from burnman import minerals
9-
10+
from burnman.utils.math import l2_norm_profile
1011

1112
if True:
1213

@@ -77,9 +78,9 @@ def calc_velocities(a, b, c):
7778
def error(a, b, c):
7879
mat_vp, mat_vs, mat_rho = calc_velocities(a, b, c)
7980

80-
vs_err = burnman.utils.math.l2(depths, mat_vs, seis_vs) / 1e9
81-
vp_err = burnman.utils.math.l2(depths, mat_vp, seis_vp) / 1e9
82-
den_err = burnman.utils.math.l2(depths, mat_rho, seis_rho) / 1e9
81+
vs_err = l2_norm_profile(depths, mat_vs, seis_vs) / 1.0e3
82+
vp_err = l2_norm_profile(depths, mat_vp, seis_vp) / 1.0e3
83+
den_err = l2_norm_profile(depths, mat_rho, seis_rho) / 1.0e3
8384

8485
return vs_err + vp_err + den_err
8586

misc/ref/performance.py.out

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
[ 0.03369626 0.00271502 0.06977656 0.00742339 0.00014545]
2-
8130.62642499
1+
[0.03369626 0.00271502 0.06977655 0.00742339 0.00014545]
2+
3183.5272394691788

0 commit comments

Comments
 (0)