Note
Go to the end to download the full example code.
query: Posterior Query Tool¶
Computes the probability, per data point, that posterior realizations satisfy a user-defined feature constraint.
The core function query(f_post_h5, query) takes a posterior HDF5
file and a query definition (dict or JSON file path) and returns an array of
probabilities – one value per data location.
try:
get_ipython()
get_ipython().run_line_magic('load_ext', 'autoreload')
get_ipython().run_line_magic('autoreload', '2')
except Exception:
pass
import os
import h5py
import numpy as np
import matplotlib.pyplot as plt
import integrate as ig
Query structure¶
A query is a dict (or JSON file) with a "constraints" list.
Constraints are evaluated sequentially (implicit AND): a realization must
pass all constraints to be counted.
Discrete-model constraint (e.g. lithology class, im=2)¶
{
::
"im": 2,
"classes": [1, 2],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 10.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": false
}
Continuous-model constraint (e.g. resistivity, im=1)¶
{
::
"im": 1,
"value_comparison": "<",
"value_threshold": 500.0,
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 0.0,
"depth_min": 0.0,
"depth_max": 100.0,
"negate": false
}
All fields:
Field |
Type |
Description |
|---|---|---|
|
int |
Prior model index (1-based) |
|
list[int] |
Class IDs to match (discrete only) |
|
str |
|
|
float |
Value threshold for continuous condition |
|
str |
|
|
str |
|
|
float |
Thickness [m] to compare against |
|
float |
Optional lower depth bound [m] |
|
float |
Optional upper depth bound [m] |
|
bool |
If True, accept realizations that do NOT satisfy the constraint |
—
Core Functions¶
The core query functions (query, query_plot, save_query, load_query, get_prior_model_info) are available from the integrate module. Access them as:
ig.query()
ig.query_plot()
ig.save_query()
ig.load_query()
ig.get_prior_model_info()
All helper functions and implementation details are in integrate/integrate_query.py
—
Examples¶
The examples below use the posterior and prior files from the examples/
directory. They are guarded with os.path.isfile so the script can be
imported without errors if the files are absent.
Select posterior hdf5 file to query (here an example the outcome of integrate_workflow.py)
f_post_h5= 'post_DAUGAARD_AVG_WF_id1_2_3_4_5_6_7_8_9_10_11_12_13.h5'
f_post_h5 = 'post_daugaard_valley_new_N1000000_dmax90_TX07_20231016_2x4_RC20-33_Nh280_Nf12_Nuse2000000_inflateNoise2.h5'
f_post_h5 = 'post_daugaard_merged_N2000000_Nuse1000000_T2_inflateNoise4.h5'
# if f_post_h5 does not exist, try to get it from the examples directory
if not os.path.isfile(f_post_h5):
files = ig.get_case_data(case='DAUGAARD', loadType='post')
f_post_h5 = files[-1]
with h5py.File(f_post_h5, 'r') as f:
f_prior_h5 = str(f.attrs.get('f5_prior', ''))
f_data_h5 = str(f.attrs.get('f5_data', ''))
ig.prior_describe(f_prior_h5)
# Select data location to plot
ip = 1000
Example 0: Sand and gravel above 30 m depth¶
Probability that the cumulative thickness of sand (class 2) and gravel (class 5) within 0–30 m depth exceeds 20 m.
query_ex0 = {
"constraints": [
{
"im": 2,
"classes": [2,5],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 20.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
}
]
}
ig.save_query(query_ex0, 'query_ex0.json')
P0, meta0 = ig.query(f_post_h5, query_ex0)
print(f"Example 0 | N_data={meta0['N_data']}, mean P={P0.mean():.3f}")
query_ex0_title = ig.title_from_json(query_ex0, f_prior_h5)
ig.query_plot(P0, meta0, title="Q0: " + query_ex0_title, hardcopy='query_ex0')
ig.query_plot(P0, meta0, ip=ip, query_dict=query_ex0, f_post_h5=f_post_h5,
title="Q0: " + query_ex0_title, hardcopy='query_ex0_ip%d' % ip)
Example 0b: Q0 with additional constraint on top layer thickness¶
Same as Q0 (sand and gravel cumulative thickness > 20m within 0-30m) BUT with an additional constraint: any top layer that is NOT sand/gravel cannot be thicker than 3m.
query_ex0b = {
"constraints": [
{
"im": 2,
"classes": [2, 5],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 20.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
},
{
"im": 2,
"classes": [1, 3, 4, 6, 7, 8], # All classes except sand (2) and gravel (5)
"thickness_mode": "first_occurrence",
"thickness_comparison": "<",
"thickness_threshold": 3.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
}
]
}
ig.save_query(query_ex0b, 'query_ex0b.json')
P0b, meta0b = ig.query(f_post_h5, query_ex0b)
print(f"Example 0b | N_data={meta0b['N_data']}, mean P={P0b.mean():.3f}")
ip_ex0b = np.argmax(P0b)
query_ex0b_title = ig.title_from_json(query_ex0b, f_prior_h5)
ig.query_plot(P0b, meta0b, title="Q0b: " + query_ex0b_title, hardcopy='query_ex0b')
ig.query_plot(P0b, meta0b, ip=ip_ex0b, query_dict=query_ex0b, f_post_h5=f_post_h5,
title="Q0b: " + query_ex0b_title, hardcopy='query_ex0b_ip%d' % ip_ex0b)
Example 0c: Q0 with minimum overburden thickness constraint¶
Same as Q0 (sand and gravel cumulative thickness > 20m within 0-30m) BUT with an additional constraint: there must be AT LEAST 2m of overburden (non-sand/gravel material) within 0-30m depth.
Overburden = all classes except sand (2) and gravel (5)
query_ex0c = {
"constraints": [
{
"im": 2,
"classes": [2, 5],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 20.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
},
{
"im": 2,
"classes": [1, 3, 4, 6, 7, 8], # Overburden: all classes except sand (2) and gravel (5)
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 2.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
}
]
}
ig.save_query(query_ex0c, 'query_ex0c.json')
P0c, meta0c = ig.query(f_post_h5, query_ex0c)
print(f"Example 0c | N_data={meta0c['N_data']}, mean P={P0c.mean():.3f}")
ip_ex0c = np.argmax(P0c)
query_ex0c_title = ig.title_from_json(query_ex0c, f_prior_h5)
ig.query_plot(P0c, meta0c, title="Q0c: " + query_ex0c_title, hardcopy='query_ex0c')
ig.query_plot(P0c, meta0c, ip=ip_ex0c, query_dict=query_ex0c, f_post_h5=f_post_h5,
title="Q0c: " + query_ex0c_title, hardcopy='query_ex0c_ip%d' % ip_ex0c)
Example 0d: Q0 with NO overburden constraint¶
Same as Q0 (sand and gravel cumulative thickness > 20m within 0-30m) BUT with an additional constraint: there can be NO overburden at the top. This means either sand (class 2) or gravel (class 5) must be the top layer.
Overburden = all classes except sand (2) and gravel (5)
query_ex0d = {
"constraints": [
{
"im": 2,
"classes": [2, 5],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 20.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
},
{
"im": 2,
"classes": [1, 3, 4, 6, 7, 8], # Overburden: all classes except sand (2) and gravel (5)
"thickness_mode": "first_occurrence",
"thickness_comparison": "<",
"thickness_threshold": 0.1, # Essentially 0 (with small numerical tolerance)
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
}
]
}
ig.save_query(query_ex0d, 'query_ex0d.json')
P0d, meta0d = ig.query(f_post_h5, query_ex0d)
print(f"Example 0d | N_data={meta0d['N_data']}, mean P={P0d.mean():.3f}")
query_ex0d_title = ig.title_from_json(query_ex0d, f_prior_h5)
# Comparison figure: all four Q0 variants side by side
fig, axes = plt.subplots(2, 2, figsize=(16, 14))
for ax, P_i, meta_i, lbl in zip(
axes.flat,
[P0, P0b, P0c, P0d],
[meta0, meta0b, meta0c, meta0d],
[f"Q0: {query_ex0_title}", f"Q0b: {query_ex0b_title}",
f"Q0c: {query_ex0c_title}", f"Q0d: {query_ex0d_title}"],
):
ax.scatter(meta_i['X'], meta_i['Y'], c='black', s=2, alpha=0.5)
sc = ax.scatter(meta_i['X'], meta_i['Y'], c=P_i, cmap='hot_r', vmin=0, vmax=1, s=1)
plt.colorbar(sc, ax=ax, label='Probability')
ax.set_xlabel('UTMX [m]')
ax.set_ylabel('UTMY [m]')
ax.set_title(lbl)
ax.set_aspect('equal')
plt.tight_layout()
plt.savefig('query_example0d_comparison.png', dpi=150)
plt.show()
ip_ex0d = np.argmax(P0d)
ig.query_plot(P0d, meta0d, title="Q0d: " + query_ex0d_title, hardcopy='query_ex0d')
ig.query_plot(P0d, meta0d, ip=ip_ex0d, query_dict=query_ex0d, f_post_h5=f_post_h5,
title="Q0d: " + query_ex0d_title, hardcopy='query_ex0d_ip%d' % ip_ex0d)
query_ex1 = {
"constraints": [
{
"im": 2,
"classes": [7],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 23.0,
"depth_min": 0.0,
"depth_max": 100.0,
"negate": False
}
]
}
ig.save_query(query_ex1, 'query_ex1.json')
P1, meta1 = ig.query(f_post_h5, query_ex1)
print(f"Example 1 | N_data={meta1['N_data']}, mean P={P1.mean():.3f}")
query_ex1_title = ig.title_from_json(query_ex1, f_prior_h5)
ig.query_plot(P1, meta1, title="Q1: " + query_ex1_title, hardcopy='query_ex1')
ig.query_plot(P1, meta1, ip=ip, query_dict=query_ex1, f_post_h5=f_post_h5,
title="Q1: " + query_ex1_title, hardcopy='query_ex1_ip%d' % ip)
Example 2: Continuous constraint¶
Probability that resistivity (im=1) is less than 100 ohm-m for a cumulative thickness of at least 25 m within 0–50 m depth.
query_ex2 = {
"constraints": [
{
"im": 1,
"value_comparison": "<",
"value_threshold": 100.0,
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 25.0,
"depth_min": 0.0,
"depth_max": 50.0,
"negate": False
}
]
}
ig.save_query(query_ex2, 'query_ex2.json')
P2, meta2 = ig.query(f_post_h5, query_ex2)
print(f"Example 2 | N_data={meta2['N_data']}, mean P={P2.mean():.3f}")
query_ex2_title = ig.title_from_json(query_ex2, f_prior_h5)
ig.query_plot(P2, meta2, title="Q2: " + query_ex2_title, hardcopy='query_ex2')
ig.query_plot(P2, meta2, ip=ip, query_dict=query_ex2, f_post_h5=f_post_h5,
title="Q2: " + query_ex2_title, hardcopy='query_ex2_ip%d' % ip)
Example 3: Combined constraint (AND)¶
Probability that:
Clay (class 2) cumulative thickness > 5 m within 0–20 m, AND
Resistivity > 500 ohm-m for at least 1 m within 20–60 m.
query_ex3 = {
"constraints": [
{
"im": 2,
"classes": [2],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 5.0,
"depth_min": 0.0,
"depth_max": 20.0,
"negate": False
},
{
"im": 1,
"value_comparison": ">",
"value_threshold": 500.0,
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 1.0,
"depth_min": 20.0,
"depth_max": 60.0,
"negate": False
}
]
}
ig.save_query(query_ex3, 'query_ex3.json')
P3, meta3 = ig.query(f_post_h5, query_ex3)
print(f"Example 3 | N_data={meta3['N_data']}, mean P={P3.mean():.3f}")
query_ex3_title = ig.title_from_json(query_ex3, f_prior_h5)
ig.query_plot(P3, meta3, title="Q3: " + query_ex3_title, hardcopy='query_ex3')
ig.query_plot(P3, meta3, ip=ip, query_dict=query_ex3, f_post_h5=f_post_h5,
title="Q3: " + query_ex3_title, hardcopy='query_ex3_ip%d' % ip)
Example 4: First-occurrence thickness¶
Probability that the first contiguous occurrence of clay (class 2) is less than 5 m thick within 0–30 m depth.
query_ex4 = {
"constraints": [
{
"im": 2,
"classes": [2],
"thickness_mode": "first_occurrence",
"thickness_comparison": "<",
"thickness_threshold": 5.0,
"depth_min": 0.0,
"depth_max": 30.0,
"negate": False
}
]
}
ig.save_query(query_ex4, 'query_ex4.json')
P4, meta4 = ig.query(f_post_h5, query_ex4)
print(f"Example 4 | N_data={meta4['N_data']}, mean P={P4.mean():.3f}")
query_ex4_title = ig.title_from_json(query_ex4, f_prior_h5)
ig.query_plot(P4, meta4, title="Q4: " + query_ex4_title, hardcopy='query_ex4')
ig.query_plot(P4, meta4, ip=ip, query_dict=query_ex4, f_post_h5=f_post_h5,
title="Q4: " + query_ex4_title, hardcopy='query_ex4_ip%d' % ip)
Example 5: Load a saved query from JSON and plot results¶
q = ig.load_query('query_ex1.json')
P5, meta5 = ig.query(f_post_h5, q)
query_ex5_title = ig.title_from_json(q, f_prior_h5)
ig.query_plot(P5, meta5, title="Q5 (loaded): " + query_ex5_title, hardcopy='query_ex5')
ig.query_plot(P5, meta5, ip=ip, query_dict=q, f_post_h5=f_post_h5,
title="Q5 (loaded): " + query_ex5_title, hardcopy='query_ex5_ip%d' % ip)
f_post_h5 = 'post_SDR_FEDL_ALL_id1.h5'
with h5py.File(f_post_h5, 'r') as f:
f_prior_h5 = str(f.attrs.get('f5_prior', ''))
f_data_h5 = str(f.attrs.get('f5_data', ''))
query_ex_wt = {
"constraints": [
{
"im": 2,
"classes": [0,1, 2,3,4,5,6,7,8],
"thickness_mode": "cumulative",
"thickness_comparison": ">",
"thickness_threshold": 5.0,
"depth_min": 0.0,
"depth_max_im": 3
}
]
}
P_wt, meta_wt = ig.query(f_post_h5, query_ex_wt)
query_ex_wt_title = ig.title_from_json(query_ex_wt, f_prior_h5)
ip_wt = np.argmax(P_wt)
ig.query_plot(P_wt, meta_wt, title="Water table: " + query_ex_wt_title, hardcopy='query_ex_wt')
ig.query_plot(P_wt, meta_wt, ip=ip_wt, query_dict=query_ex_wt, f_post_h5=f_post_h5,
title="Water table: " + query_ex_wt_title, hardcopy='query_ex_wt_ip%d' % ip_wt)
Example 6: Percentile query — cumulative thickness distribution¶
Instead of asking “what is the probability that Sand+Grus exceeds X m?”, ask “what are the p5, p50, p95 of the cumulative Sand+Grus thickness?”.
The query dict uses a "metric" key instead of "constraints".
The thickness comparison fields are omitted — we want the raw distribution.
query_pct = {
"metric": {
"im": 2,
"classes": [1, 2], # Sand and Grus
"thickness_mode": "cumulative",
"depth_min": 0.0,
"depth_max": 30.0
},
"percentiles": [5, 50, 95]
}
ig.save_query(query_pct, 'query_pct.json')
pct_values, meta_pct = ig.query_percentile(f_post_h5, query_pct)
# pct_values shape: (N_data, 3) — columns are P5, P50, P95
p5 = pct_values[:, 0]
p50 = pct_values[:, 1]
p95 = pct_values[:, 2]
print(f"Sand+Grus thickness (0-30 m) | "
f"P5={p5.mean():.1f} m P50={p50.mean():.1f} m P95={p95.mean():.1f} m (spatial mean)")
query_pct_title = ig.title_from_json(query_pct, f_prior_h5)
ig.query_percentile_plot(pct_values, meta_pct,
query_text=query_pct_title,
hardcopy='query_pct_sand_grus')
Example 7: Percentile query — cross-model depth bound¶
P5/P50/P95 of the cumulative Sand+Grus thickness above the water table
(im=3 is a scalar model storing the water-table depth per realization).
depth_max_im: 3 sets the upper depth cutoff to the water-table value of
each realization individually.
query_pct_wl = {
"metric": {
"im": 2,
"classes": [1, 2], # Sand and Grus
"thickness_mode": "cumulative",
"depth_min": 0.0,
"depth_max_im": 3 # per-realization depth cutoff = Waterlevel
},
"percentiles": [5, 50, 95]
}
pct_wl, meta_pct_wl = ig.query_percentile(f_post_h5, query_pct_wl)
p50_wl = pct_wl[:, 1]
print(f"Sand+Grus above water table | P50 spatial mean = {p50_wl.mean():.1f} m")
query_pct_wl_title = ig.title_from_json(query_pct_wl, f_prior_h5)
ig.query_percentile_plot(pct_wl, meta_pct_wl,
query_text=query_pct_wl_title,
hardcopy='query_pct_sand_grus_above_wl')