|
| 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