|
36 | 36 | StatusHistory, |
37 | 37 | MonitoringFrequencyHistory, |
38 | 38 | MeasuringPointHistory, |
| 39 | + AquiferSystem, |
| 40 | + AquiferType, |
| 41 | + GeologicFormation, |
| 42 | + ThingAquiferAssociation, |
| 43 | + ThingGeologicFormationAssociation, |
39 | 44 | ) |
40 | 45 | from schemas.thing import CreateWell, CreateWellScreen |
41 | 46 | from services.gcs_helper import get_storage_bucket |
@@ -117,6 +122,107 @@ def _extract_casing_materials(row) -> list[str]: |
117 | 122 | return materials |
118 | 123 |
|
119 | 124 |
|
| 125 | +# Parse aquifer codes |
| 126 | +def _extract_aquifer_type_codes(aquifer_code: str) -> list[str]: |
| 127 | + """ |
| 128 | + Parse aquifer type codes that may contain multiple values. |
| 129 | +
|
| 130 | + Args: |
| 131 | + aquifer_code: Raw code from AquiferType field |
| 132 | +
|
| 133 | + Returns: |
| 134 | + List of individual codes |
| 135 | + """ |
| 136 | + if not aquifer_code: |
| 137 | + return [] |
| 138 | + # clean the code |
| 139 | + code = aquifer_code.strip().upper() |
| 140 | + # split into individual characters. This handles cases like "FC" -> ["F", "C"] |
| 141 | + individual_codes = list(code) |
| 142 | + return individual_codes |
| 143 | + |
| 144 | + |
| 145 | +# Get or create aquifer system |
| 146 | +def get_or_create_aquifer_system( |
| 147 | + session: Session, aquifer_name: str, primary_type: str |
| 148 | +) -> AquiferSystem | None: |
| 149 | + """ |
| 150 | + Get existing aquifer or create new one if it doesn't exist. |
| 151 | +
|
| 152 | + With the new AquiferType model, we create ONE aquifer record per named |
| 153 | + aquifer (e.g., one "Santa Fe Group"), not multiple variants. |
| 154 | +
|
| 155 | + Args: |
| 156 | + session: Database session |
| 157 | + aquifer_name: Name of the aquifer (from AqClass or type name) |
| 158 | + primary_type: Primary aquifer type for the aquifer_type field |
| 159 | + """ |
| 160 | + # Try to find existing aquifer by name |
| 161 | + aquifer = ( |
| 162 | + session.query(AquiferSystem).filter(AquiferSystem.name == aquifer_name).first() |
| 163 | + ) |
| 164 | + |
| 165 | + if aquifer: |
| 166 | + return aquifer |
| 167 | + |
| 168 | + # Create new aquifer |
| 169 | + try: |
| 170 | + logger.info( |
| 171 | + f"Creating new aquifer system: {aquifer_name} (primary type: {primary_type})" |
| 172 | + ) |
| 173 | + |
| 174 | + aquifer = AquiferSystem( |
| 175 | + name=aquifer_name, |
| 176 | + aquifer_type=primary_type, # Primary type |
| 177 | + geographic_scale=None, # Default |
| 178 | + ) |
| 179 | + session.add(aquifer) |
| 180 | + session.flush() # Get the ID |
| 181 | + return aquifer |
| 182 | + except Exception as e: |
| 183 | + logger.critical(f"Error creating aquifer {aquifer_name}: {e}") |
| 184 | + return None |
| 185 | + |
| 186 | + |
| 187 | +def get_or_create_geologic_formation( |
| 188 | + session: Session, formation_code: str |
| 189 | +) -> GeologicFormation | None: |
| 190 | + """ |
| 191 | + Get existing geologic formation or create new one if it doesn't exist. |
| 192 | +
|
| 193 | + Args: |
| 194 | + session: Database session |
| 195 | + formation_code: The formation code from FormationZone field |
| 196 | +
|
| 197 | + Returns: |
| 198 | + GeologicFormation object or None if creation fails |
| 199 | + """ |
| 200 | + # Try to find existing formation |
| 201 | + formation = ( |
| 202 | + session.query(GeologicFormation) |
| 203 | + .filter(GeologicFormation.formation_code == formation_code) |
| 204 | + .first() |
| 205 | + ) |
| 206 | + |
| 207 | + if formation: |
| 208 | + return formation |
| 209 | + |
| 210 | + # If not found, create new formation |
| 211 | + try: |
| 212 | + logger.info(f"Creating new geologic formation: {formation_code}") |
| 213 | + formation = GeologicFormation( |
| 214 | + formation_code=formation_code, |
| 215 | + description=None, |
| 216 | + lithology=None, |
| 217 | + ) |
| 218 | + session.add(formation) |
| 219 | + session.flush() |
| 220 | + return formation |
| 221 | + except Exception as e: |
| 222 | + logger.critical(f"Error creating formation {formation_code}: {e}") |
| 223 | + return None |
| 224 | + |
| 225 | + |
120 | 226 | def get_wells_to_transfer( |
121 | 227 | sess: Session, flags: dict = None |
122 | 228 | ) -> tuple[pd.DataFrame, pd.DataFrame]: |
@@ -330,6 +436,131 @@ def transfer_wells(session: Session, flags: dict = None, limit: int = 0) -> None |
330 | 436 | assoc.thing = well |
331 | 437 | session.add(assoc) |
332 | 438 |
|
| 439 | + # --- Create Aquifer Association with AquiferType records --- |
| 440 | + if hasattr(row, "AquiferType") and not isna(row.AquiferType): |
| 441 | + try: |
| 442 | + # Parse codes (handles multi-character codes like "FC") |
| 443 | + aquifer_codes = _extract_aquifer_type_codes(row.AquiferType) |
| 444 | + |
| 445 | + if not aquifer_codes: |
| 446 | + logger.warning( |
| 447 | + f"Well {row.PointID}: Empty aquifer codes after parsing '{row.AquiferType}'" |
| 448 | + ) |
| 449 | + else: |
| 450 | + # Map AqClass code to aquifer name using lexicon mapper |
| 451 | + if hasattr(row, "AqClass") and not isna(row.AqClass): |
| 452 | + try: |
| 453 | + aquifer_name = lexicon_mapper.map_value( |
| 454 | + f"LU_AquiferClass:{row.AqClass}" |
| 455 | + ) |
| 456 | + except KeyError: |
| 457 | + logger.warning( |
| 458 | + f"Unknown AqClass code '{row.AqClass}' for well {row.PointID}, using first type as name" |
| 459 | + ) |
| 460 | + aquifer_name = lexicon_mapper.map_value( |
| 461 | + f"LU_AquiferType:{aquifer_codes[0]}" |
| 462 | + ) |
| 463 | + else: |
| 464 | + # No AqClass - use first code's mapped name as aquifer name |
| 465 | + aquifer_name = lexicon_mapper.map_value( |
| 466 | + f"LU_AquiferType:{aquifer_codes[0]}" |
| 467 | + ) |
| 468 | + |
| 469 | + # Determine primary type |
| 470 | + try: |
| 471 | + primary_type = lexicon_mapper.map_value( |
| 472 | + f"LU_AquiferType:{aquifer_codes[0]}" |
| 473 | + ) |
| 474 | + except KeyError: |
| 475 | + logger.warning( |
| 476 | + f"Unknown aquifer type code '{aquifer_codes[0]}' for well {row.PointID}" |
| 477 | + ) |
| 478 | + primary_type = None |
| 479 | + |
| 480 | + if primary_type: |
| 481 | + # Get or create the aquifer |
| 482 | + aquifer = get_or_create_aquifer_system( |
| 483 | + session, aquifer_name, primary_type |
| 484 | + ) |
| 485 | + |
| 486 | + if aquifer: |
| 487 | + # Check if association already exists |
| 488 | + existing_assoc = ( |
| 489 | + session.query(ThingAquiferAssociation) |
| 490 | + .filter( |
| 491 | + ThingAquiferAssociation.thing_id == well.id, |
| 492 | + ThingAquiferAssociation.aquifer_system_id |
| 493 | + == aquifer.id, |
| 494 | + ) |
| 495 | + .first() |
| 496 | + ) |
| 497 | + |
| 498 | + if not existing_assoc: |
| 499 | + # Create the association |
| 500 | + aquifer_assoc = ThingAquiferAssociation( |
| 501 | + thing=well, aquifer_system=aquifer |
| 502 | + ) |
| 503 | + session.add(aquifer_assoc) |
| 504 | + session.flush() |
| 505 | + |
| 506 | + # Create AquiferType records for EACH characteristic |
| 507 | + aquifer_type_names = [] |
| 508 | + for aquifer_code in aquifer_codes: |
| 509 | + try: |
| 510 | + type_name = lexicon_mapper.map_value( |
| 511 | + f"LU_AquiferType:{aquifer_code}" |
| 512 | + ) |
| 513 | + aquifer_type = AquiferType( |
| 514 | + thing_aquifer_association=aquifer_assoc, |
| 515 | + aquifer_type=type_name, |
| 516 | + ) |
| 517 | + session.add(aquifer_type) |
| 518 | + aquifer_type_names.append(type_name) |
| 519 | + except KeyError: |
| 520 | + logger.warning( |
| 521 | + f"Unknown aquifer code '{aquifer_code}' from AquiferType='{row.AquiferType}' " |
| 522 | + f"for well {well.name}. Skipping this code." |
| 523 | + ) |
| 524 | + |
| 525 | + logger.info( |
| 526 | + f"Associated well {well.name} with aquifer {aquifer.name} " |
| 527 | + f"(types: {', '.join(aquifer_type_names)})" |
| 528 | + ) |
| 529 | + |
| 530 | + except Exception as e: |
| 531 | + logger.critical( |
| 532 | + f"Error creating aquifer associations for {well.name}: {e}" |
| 533 | + ) |
| 534 | + |
| 535 | + # --- Create Formation Association (if FormationZone exists) --- |
| 536 | + if hasattr(row, "FormationZone") and not isna(row.FormationZone): |
| 537 | + try: |
| 538 | + formation_code = row.FormationZone |
| 539 | + formation = get_or_create_geologic_formation(session, formation_code) |
| 540 | + |
| 541 | + if formation: |
| 542 | + top_depth = 0.0 |
| 543 | + bottom_depth = ( |
| 544 | + row.WellDepth |
| 545 | + if row.WellDepth and not isna(row.WellDepth) |
| 546 | + else 100.0 |
| 547 | + ) |
| 548 | + |
| 549 | + formation_assoc = ThingGeologicFormationAssociation( |
| 550 | + thing=well, |
| 551 | + geologic_formation=formation, |
| 552 | + top_depth=top_depth, |
| 553 | + bottom_depth=bottom_depth, |
| 554 | + ) |
| 555 | + session.add(formation_assoc) |
| 556 | + logger.info( |
| 557 | + f"Associated well {well.name} with formation {formation.formation_code} (0-{bottom_depth} ft)" |
| 558 | + ) |
| 559 | + except Exception as e: |
| 560 | + logger.critical( |
| 561 | + f"Error creating formation association for {well.name}: {e}" |
| 562 | + ) |
| 563 | + |
333 | 564 | session.commit() |
334 | 565 |
|
335 | 566 | # add things thate need well id |
|
0 commit comments