Skip to content

Commit 693976e

Browse files
committed
docs: examples
1 parent 1dc6c68 commit 693976e

File tree

3 files changed

+849
-0
lines changed

3 files changed

+849
-0
lines changed
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
"""
2+
Example: Using Geological Observations in Scenarios
3+
4+
This example demonstrates the new observation system for defining
5+
geological data in an intuitive, geologically-meaningful way.
6+
"""
7+
8+
import numpy as np
9+
from LoopStructural.modelling.core.geological_scenario import GeologicalScenario
10+
from LoopStructural.modelling.core.geological_observations import ObservationCollection
11+
12+
# ==============================================================================
13+
# Setup: Model domain
14+
# ==============================================================================
15+
16+
origin = np.array([0, 0, 0])
17+
maximum = np.array([1000, 1000, 500])
18+
19+
# ==============================================================================
20+
# Example 1: Stratigraphic Units with Various Observation Types
21+
# ==============================================================================
22+
23+
print("=" * 70)
24+
print("Example 1: Stratigraphic Unit Observations")
25+
print("=" * 70)
26+
27+
scenario = GeologicalScenario(origin, maximum)
28+
29+
# Add units
30+
scenario.add_unit('basement')
31+
scenario.add_unit('sandstone')
32+
scenario.add_unit('shale')
33+
scenario.add_unit('limestone')
34+
35+
# Define conformable sequence
36+
scenario.add_conformable_sequence(['basement', 'sandstone', 'shale', 'limestone'])
37+
38+
# ===== Add observations for sandstone unit =====
39+
print("\nAdding observations for 'sandstone' unit:")
40+
41+
# Method 1: Fluent interface
42+
scenario.observations('sandstone')\
43+
.add_contact([100, 100, 50], comment="Drillhole 1")\
44+
.add_contact([200, 150, 45], comment="Drillhole 2")\
45+
.add_contact([300, 200, 48], comment="Drillhole 3")\
46+
.add_orientation([150, 125, 47], strike=45, dip=15, comment="Outcrop measurement")\
47+
.add_orientation([250, 175, 46], strike=50, dip=18, comment="Outcrop measurement")\
48+
.add_above_point([150, 125, 60], comment="Known to be above contact")\
49+
.add_below_point([150, 125, 30], comment="Known to be below contact")\
50+
.set_thickness(25.0)
51+
52+
print(f" Added {len(scenario.observations('sandstone'))} observations for sandstone")
53+
54+
# ===== Add observations for shale unit =====
55+
print("\nAdding observations for 'shale' unit:")
56+
57+
# Method 2: Get collection and add observations
58+
shale_obs = scenario.observations('shale')
59+
60+
# Contact points from drillholes
61+
contact_points = [
62+
[100, 100, 75],
63+
[200, 150, 70],
64+
[300, 200, 73],
65+
[150, 300, 72]
66+
]
67+
for i, point in enumerate(contact_points, 1):
68+
shale_obs.add_contact(point, comment=f"Drillhole {i}")
69+
70+
# Orientation measurements from outcrops
71+
orientations = [
72+
{'location': [150, 125, 72], 'strike': 50, 'dip': 20},
73+
{'location': [250, 200, 71], 'strike': 48, 'dip': 22},
74+
]
75+
for orient in orientations:
76+
shale_obs.add_orientation(
77+
location=orient['location'],
78+
strike=orient['strike'],
79+
dip=orient['dip'],
80+
weight=2.0, # Higher weight for good outcrop data
81+
comment="Outcrop"
82+
)
83+
84+
# Gradient vectors (from seismic interpretation)
85+
shale_obs.add_orientation(
86+
location=[400, 400, 70],
87+
gradient=[0.05, 0.1, 0.9], # Approximate gradient vector
88+
comment="Seismic interpretation"
89+
)
90+
91+
# Inequality constraints
92+
shale_obs.add_inside_point([200, 200, 71], comment="Core sample confirms shale")
93+
94+
print(f" Added {len(shale_obs)} observations for shale")
95+
96+
# ===== Add observations for limestone unit =====
97+
print("\nAdding observations for 'limestone' unit:")
98+
99+
limestone_obs = scenario.observations('limestone')
100+
101+
# Using gradient and tangent vectors
102+
limestone_obs\
103+
.add_contact([100, 100, 95])\
104+
.add_contact([200, 150, 92])\
105+
.add_orientation([150, 125, 93], strike=52, dip=18)\
106+
.add_orientation([250, 175, 91], gradient=[0.03, 0.08, 0.95])\
107+
.add_orientation([350, 225, 90], tangent=[1.0, 0.0, -0.03], comment="Tangent to bedding")
108+
109+
print(f" Added {len(limestone_obs)} observations for limestone")
110+
111+
# ==============================================================================
112+
# Example 2: Fault Observations
113+
# ==============================================================================
114+
115+
print("\n" + "=" * 70)
116+
print("Example 2: Fault Observations")
117+
print("=" * 70)
118+
119+
# Add fault with fault-specific observations
120+
scenario.add_fault('main_fault', displacement=100, cuts=['sandstone', 'shale'])
121+
122+
print("\nAdding fault-specific observations:")
123+
124+
fault_obs = scenario.observations('main_fault')
125+
126+
# Fault trace observations (surface intersections)
127+
fault_trace_points = [
128+
[150, 100, 0], # Surface trace point 1
129+
[200, 200, 0], # Surface trace point 2
130+
[250, 300, 0], # Surface trace point 3
131+
]
132+
for point in fault_trace_points:
133+
fault_obs.add_fault_trace(point, comment="Mapped trace")
134+
135+
# Fault orientation measurements
136+
fault_obs\
137+
.add_fault_orientation([200, 200, 50], strike=90, dip=60, comment="Outcrop")\
138+
.add_fault_orientation([250, 250, 75], strike=88, dip=62, comment="Drillhole intersection")
139+
140+
# Hanging wall and footwall observations
141+
fault_obs\
142+
.add_hangingwall_point([220, 200, 50], comment="Confirmed hanging wall")\
143+
.add_footwall_point([180, 200, 50], comment="Confirmed footwall")
144+
145+
# Slip vector observation
146+
fault_obs.add_slip_vector(
147+
[200, 200, 50],
148+
slip_vector=[0.0, 0.0, -1.0], # Pure dip slip
149+
comment="Slickensides measurement"
150+
)
151+
152+
# Displacement measurements
153+
fault_obs.add_displacement(
154+
[200, 200, 50],
155+
displacement=100,
156+
direction=[0.0, 0.0, -1.0],
157+
comment="Offset marker bed"
158+
)
159+
160+
print(f" Added {len(fault_obs)} fault observations")
161+
162+
# ==============================================================================
163+
# Example 3: Mixing Observation Methods
164+
# ==============================================================================
165+
166+
print("\n" + "=" * 70)
167+
print("Example 3: Mixing Traditional and New Observation Methods")
168+
print("=" * 70)
169+
170+
# You can also add observations from traditional DataFrames
171+
import pandas as pd
172+
173+
# Traditional LoopStructural DataFrame format
174+
traditional_data = pd.DataFrame({
175+
'feature_name': ['basement'] * 5,
176+
'X': [100, 200, 300, 400, 500],
177+
'Y': [100, 150, 200, 250, 300],
178+
'Z': [10, 12, 11, 13, 12],
179+
'val': [np.nan] * 5, # Interface points
180+
'nx': [np.nan] * 5,
181+
'ny': [np.nan] * 5,
182+
'nz': [np.nan] * 5,
183+
'gx': [np.nan] * 5,
184+
'gy': [np.nan] * 5,
185+
'gz': [np.nan] * 5,
186+
'tx': [np.nan] * 5,
187+
'ty': [np.nan] * 5,
188+
'tz': [np.nan] * 5,
189+
'w': [1.0] * 5,
190+
'coord': [0] * 5, # Interface constraint
191+
'polarity': [1.0] * 5,
192+
})
193+
194+
# Add traditional data
195+
scenario.add_observations_from_dataframe('basement', traditional_data)
196+
197+
# And still add new-style observations
198+
scenario.observations('basement')\
199+
.add_orientation([300, 200, 11], strike=48, dip=12)\
200+
.add_orientation([400, 250, 13], strike=50, dip=10)
201+
202+
print("✓ Combined traditional DataFrame with new-style observations")
203+
204+
# ==============================================================================
205+
# Example 4: Getting Combined Data
206+
# ==============================================================================
207+
208+
print("\n" + "=" * 70)
209+
print("Example 4: Viewing All Observations")
210+
print("=" * 70)
211+
212+
# Get all observations as a combined DataFrame
213+
all_obs = scenario.get_all_observations_dataframe()
214+
215+
print(f"\nTotal observations: {len(all_obs)}")
216+
print(f"Features with observations: {all_obs['feature_name'].unique().tolist()}")
217+
print(f"\nObservation types:")
218+
print(all_obs.groupby('feature_name').size())
219+
220+
# Show summary statistics
221+
print(f"\nSummary:")
222+
print(f" Contact points (coord=0): {len(all_obs[all_obs['coord'] == 0])}")
223+
print(f" Orientation constraints (coord=1): {len(all_obs[all_obs['coord'] == 1])}")
224+
print(f" Inequality constraints (coord=2): {len(all_obs[all_obs['coord'] == 2])}")
225+
226+
# ==============================================================================
227+
# Example 5: Building the Model with Observations
228+
# ==============================================================================
229+
230+
print("\n" + "=" * 70)
231+
print("Example 5: Building Model from Observations")
232+
print("=" * 70)
233+
234+
# Validate the scenario
235+
print("\nValidating scenario...")
236+
try:
237+
warnings = scenario.validate()
238+
if warnings:
239+
print(f" Warnings: {warnings}")
240+
else:
241+
print(" ✓ Validation passed")
242+
except ValueError as e:
243+
print(f" ✗ Validation failed: {e}")
244+
245+
# Build the model
246+
print("\nBuilding model...")
247+
# model = scenario.build() # Would build with all observations
248+
# model.update() # Would run interpolation
249+
print(" ✓ Model would be built with all observations included")
250+
251+
# ==============================================================================
252+
# Example 6: Advanced - Programmatic Observation Generation
253+
# ==============================================================================
254+
255+
print("\n" + "=" * 70)
256+
print("Example 6: Programmatic Observation Generation")
257+
print("=" * 70)
258+
259+
# Create a new unit with programmatically generated observations
260+
scenario2 = GeologicalScenario(origin, maximum)
261+
scenario2.add_unit('synthetic_unit')
262+
263+
obs = scenario2.observations('synthetic_unit')
264+
265+
# Generate synthetic contact points along a dipping surface
266+
n_points = 20
267+
for i in range(n_points):
268+
x = np.random.uniform(0, 1000)
269+
y = np.random.uniform(0, 1000)
270+
z = 100 + 0.1 * x + 0.05 * y + np.random.normal(0, 2) # Dipping plane + noise
271+
272+
obs.add_contact([x, y, z], weight=1.0)
273+
274+
# Add orientations at regular grid
275+
for x in range(100, 1000, 200):
276+
for y in range(100, 1000, 200):
277+
z = 100 + 0.1 * x + 0.05 * y
278+
obs.add_orientation([x, y, z], strike=45, dip=6, weight=2.0)
279+
280+
print(f"Generated {len(obs)} synthetic observations")
281+
282+
# ==============================================================================
283+
# Summary
284+
# ==============================================================================
285+
286+
print("\n" + "=" * 70)
287+
print("SUMMARY: Observation System Benefits")
288+
print("=" * 70)
289+
290+
print("""
291+
✓ Geological Intuition: Use geological terminology
292+
- add_contact(), add_orientation(), add_inside_point()
293+
- add_fault_trace(), add_hangingwall_point()
294+
295+
✓ Flexible Data Entry:
296+
- Fluent interface for method chaining
297+
- Individual observations or programmatic generation
298+
- Mix with traditional DataFrames
299+
300+
✓ Type Safety: Specific observation types prevent errors
301+
- ContactObservation, OrientationObservation, etc.
302+
- Validation at creation time
303+
304+
✓ Metadata Support: Add comments and weights
305+
- Track data source and quality
306+
- Weight observations by confidence
307+
308+
✓ Automatic Conversion: Converts to LoopStructural format
309+
- No manual DataFrame construction
310+
- Handles coord, polarity, etc. automatically
311+
312+
✓ Feature-Specific: Observations attached to features
313+
- Clear association between data and geology
314+
- Easy to review what data supports each feature
315+
""")
316+
317+
print("\n" + "=" * 70)
318+
print("Example complete!")
319+
print("=" * 70)

0 commit comments

Comments
 (0)