gea-program 0.47.0__tar.gz → 0.49.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. {gea_program-0.47.0 → gea_program-0.49.0}/PKG-INFO +30 -1
  2. {gea_program-0.47.0 → gea_program-0.49.0}/README.md +29 -0
  3. {gea_program-0.47.0 → gea_program-0.49.0}/gea/__init__.py +1 -1
  4. {gea_program-0.47.0 → gea_program-0.49.0}/gea/__main__.py +63 -3
  5. {gea_program-0.47.0 → gea_program-0.49.0}/gea/acceptance_tests.py +385 -0
  6. {gea_program-0.47.0 → gea_program-0.49.0}/gea/alerts.py +20 -2
  7. gea_program-0.49.0/gea/collision.py +344 -0
  8. {gea_program-0.47.0 → gea_program-0.49.0}/gea/declarations.py +136 -2
  9. {gea_program-0.47.0 → gea_program-0.49.0}/gea/dossier.py +36 -2
  10. gea_program-0.49.0/gea/geomag.py +122 -0
  11. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/alerts.md +1 -1
  12. gea_program-0.49.0/gea/help/collision.md +19 -0
  13. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/declare.md +2 -2
  14. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/dossier.md +2 -0
  15. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/invoice-check.md +1 -1
  16. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/pad-summary.md +1 -1
  17. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/rig-time.md +3 -1
  18. {gea_program-0.47.0 → gea_program-0.49.0}/gea/helplib.py +2 -1
  19. {gea_program-0.47.0 → gea_program-0.49.0}/gea/invoice_check.py +25 -7
  20. {gea_program-0.47.0 → gea_program-0.49.0}/gea/notify.py +3 -1
  21. {gea_program-0.47.0 → gea_program-0.49.0}/gea/pad_summary.py +41 -4
  22. gea_program-0.49.0/gea/reference/WMM2025.COF +93 -0
  23. gea_program-0.49.0/gea/reference/iscwsa_rev4_example.json +1751 -0
  24. {gea_program-0.47.0 → gea_program-0.49.0}/gea/rig_state.py +144 -8
  25. {gea_program-0.47.0 → gea_program-0.49.0}/gea/service.py +31 -1
  26. {gea_program-0.47.0 → gea_program-0.49.0}/gea/web/app.html +62 -8
  27. gea_program-0.49.0/gea/wellpos.py +286 -0
  28. gea_program-0.49.0/gea/xlsx_read.py +205 -0
  29. {gea_program-0.47.0 → gea_program-0.49.0}/gea_program.egg-info/PKG-INFO +30 -1
  30. {gea_program-0.47.0 → gea_program-0.49.0}/gea_program.egg-info/SOURCES.txt +7 -0
  31. {gea_program-0.47.0 → gea_program-0.49.0}/pyproject.toml +1 -1
  32. {gea_program-0.47.0 → gea_program-0.49.0}/LICENSE +0 -0
  33. {gea_program-0.47.0 → gea_program-0.49.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  34. {gea_program-0.47.0 → gea_program-0.49.0}/gea/accuracy_statement.py +0 -0
  35. {gea_program-0.47.0 → gea_program-0.49.0}/gea/alarm_engine.py +0 -0
  36. {gea_program-0.47.0 → gea_program-0.49.0}/gea/backup.py +0 -0
  37. {gea_program-0.47.0 → gea_program-0.49.0}/gea/bench.py +0 -0
  38. {gea_program-0.47.0 → gea_program-0.49.0}/gea/blind_harness.py +0 -0
  39. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  40. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  41. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  42. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  43. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  44. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  45. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  46. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  47. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  48. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  49. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  50. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  51. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  52. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  53. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  54. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  55. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  56. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  57. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  58. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  59. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  60. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  61. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  62. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  63. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  64. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  65. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  66. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  67. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  68. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  69. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  70. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  71. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  72. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  73. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  74. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  75. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  76. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  77. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  78. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  79. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  80. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  81. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  82. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  83. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  84. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  85. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  86. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  87. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  88. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  89. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  90. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  91. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  92. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  93. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  94. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  95. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  96. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  97. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  98. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  99. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  100. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  101. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  102. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  103. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  104. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  105. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  106. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/med_160_sapropels.txt +0 -0
  107. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  108. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  109. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  110. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  111. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  112. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  113. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  114. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  115. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  116. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  117. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  118. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  119. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  120. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  121. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  122. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  123. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  124. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  125. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  126. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  127. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  128. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  129. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  130. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  131. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  132. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  133. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  134. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  135. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  136. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  137. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  138. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  139. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  140. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  141. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  142. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  143. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog_map.py +0 -0
  144. {gea_program-0.47.0 → gea_program-0.49.0}/gea/catalog_register.py +0 -0
  145. {gea_program-0.47.0 → gea_program-0.49.0}/gea/certificates.py +0 -0
  146. {gea_program-0.47.0 → gea_program-0.49.0}/gea/cli.py +0 -0
  147. {gea_program-0.47.0 → gea_program-0.49.0}/gea/client_reports.py +0 -0
  148. {gea_program-0.47.0 → gea_program-0.49.0}/gea/config_versioning.py +0 -0
  149. {gea_program-0.47.0 → gea_program-0.49.0}/gea/conformance.py +0 -0
  150. {gea_program-0.47.0 → gea_program-0.49.0}/gea/control_room.py +0 -0
  151. {gea_program-0.47.0 → gea_program-0.49.0}/gea/correlation.py +0 -0
  152. {gea_program-0.47.0 → gea_program-0.49.0}/gea/dashboard.py +0 -0
  153. {gea_program-0.47.0 → gea_program-0.49.0}/gea/deviation.py +0 -0
  154. {gea_program-0.47.0 → gea_program-0.49.0}/gea/dlis.py +0 -0
  155. {gea_program-0.47.0 → gea_program-0.49.0}/gea/dnp3.py +0 -0
  156. {gea_program-0.47.0 → gea_program-0.49.0}/gea/doctor.py +0 -0
  157. {gea_program-0.47.0 → gea_program-0.49.0}/gea/dossier_verify.py +0 -0
  158. {gea_program-0.47.0 → gea_program-0.49.0}/gea/downhole_engine.py +0 -0
  159. {gea_program-0.47.0 → gea_program-0.49.0}/gea/drift_monitor.py +0 -0
  160. {gea_program-0.47.0 → gea_program-0.49.0}/gea/drill_string.py +0 -0
  161. {gea_program-0.47.0 → gea_program-0.49.0}/gea/earth_model.py +0 -0
  162. {gea_program-0.47.0 → gea_program-0.49.0}/gea/economics.py +0 -0
  163. {gea_program-0.47.0 → gea_program-0.49.0}/gea/ed25519.py +0 -0
  164. {gea_program-0.47.0 → gea_program-0.49.0}/gea/edr.py +0 -0
  165. {gea_program-0.47.0 → gea_program-0.49.0}/gea/elevation.py +0 -0
  166. {gea_program-0.47.0 → gea_program-0.49.0}/gea/etp.py +0 -0
  167. {gea_program-0.47.0 → gea_program-0.49.0}/gea/etp12_schemas.json +0 -0
  168. {gea_program-0.47.0 → gea_program-0.49.0}/gea/example_register_map.json +0 -0
  169. {gea_program-0.47.0 → gea_program-0.49.0}/gea/fat_sat.py +0 -0
  170. {gea_program-0.47.0 → gea_program-0.49.0}/gea/files.py +0 -0
  171. {gea_program-0.47.0 → gea_program-0.49.0}/gea/fixtures/README.md +0 -0
  172. {gea_program-0.47.0 → gea_program-0.49.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  173. {gea_program-0.47.0 → gea_program-0.49.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  174. {gea_program-0.47.0 → gea_program-0.49.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  175. {gea_program-0.47.0 → gea_program-0.49.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  176. {gea_program-0.47.0 → gea_program-0.49.0}/gea/follower.py +0 -0
  177. {gea_program-0.47.0 → gea_program-0.49.0}/gea/forward_model.py +0 -0
  178. {gea_program-0.47.0 → gea_program-0.49.0}/gea/friction_priors.py +0 -0
  179. {gea_program-0.47.0 → gea_program-0.49.0}/gea/gamma.py +0 -0
  180. {gea_program-0.47.0 → gea_program-0.49.0}/gea/gauge_aging.py +0 -0
  181. {gea_program-0.47.0 → gea_program-0.49.0}/gea/gauge_specs.py +0 -0
  182. {gea_program-0.47.0 → gea_program-0.49.0}/gea/geodesy.py +0 -0
  183. {gea_program-0.47.0 → gea_program-0.49.0}/gea/gravity_reference.py +0 -0
  184. {gea_program-0.47.0 → gea_program-0.49.0}/gea/hartip.py +0 -0
  185. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/TESTER_GUIDE.md +0 -0
  186. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/alarms.md +0 -0
  187. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/audit-update.md +0 -0
  188. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/backup.md +0 -0
  189. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/catalog.md +0 -0
  190. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/charts.md +0 -0
  191. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/conformance.md +0 -0
  192. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/control-room.md +0 -0
  193. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/data-in.md +0 -0
  194. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/dlis.md +0 -0
  195. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/doctor.md +0 -0
  196. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/drift.md +0 -0
  197. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/drillstring.md +0 -0
  198. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/economics.md +0 -0
  199. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/edr.md +0 -0
  200. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/etp.md +0 -0
  201. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/field-ports.md +0 -0
  202. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/files.md +0 -0
  203. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/historian.md +0 -0
  204. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/instruments.md +0 -0
  205. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/interval.md +0 -0
  206. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/map.md +0 -0
  207. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/month-end.md +0 -0
  208. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/notifications.md +0 -0
  209. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/opseis.md +0 -0
  210. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/osdu.md +0 -0
  211. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/patches.md +0 -0
  212. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/penetrator.md +0 -0
  213. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/ppdm.md +0 -0
  214. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/quakeml.md +0 -0
  215. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/quality.md +0 -0
  216. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/report-history.md +0 -0
  217. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/seedlink.md +0 -0
  218. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/seismic.md +0 -0
  219. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/setup.md +0 -0
  220. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/simulation.md +0 -0
  221. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/site.md +0 -0
  222. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/sites.md +0 -0
  223. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/slip.md +0 -0
  224. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/soak.md +0 -0
  225. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/sra.md +0 -0
  226. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/start.md +0 -0
  227. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/structure.md +0 -0
  228. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/tools.md +0 -0
  229. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/transients.md +0 -0
  230. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/upkeep.md +0 -0
  231. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/vibration.md +0 -0
  232. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/well-report.md +0 -0
  233. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/well-tests.md +0 -0
  234. {gea_program-0.47.0 → gea_program-0.49.0}/gea/help/well3d.md +0 -0
  235. {gea_program-0.47.0 → gea_program-0.49.0}/gea/housekeeping.py +0 -0
  236. {gea_program-0.47.0 → gea_program-0.49.0}/gea/interval.py +0 -0
  237. {gea_program-0.47.0 → gea_program-0.49.0}/gea/inverse_engine.py +0 -0
  238. {gea_program-0.47.0 → gea_program-0.49.0}/gea/jobs.py +0 -0
  239. {gea_program-0.47.0 → gea_program-0.49.0}/gea/live_ports.py +0 -0
  240. {gea_program-0.47.0 → gea_program-0.49.0}/gea/loadtest.py +0 -0
  241. {gea_program-0.47.0 → gea_program-0.49.0}/gea/matplotlib_demo.py +0 -0
  242. {gea_program-0.47.0 → gea_program-0.49.0}/gea/modbus.py +0 -0
  243. {gea_program-0.47.0 → gea_program-0.49.0}/gea/modbus_rtu.py +0 -0
  244. {gea_program-0.47.0 → gea_program-0.49.0}/gea/model_card.py +0 -0
  245. {gea_program-0.47.0 → gea_program-0.49.0}/gea/month_end.py +0 -0
  246. {gea_program-0.47.0 → gea_program-0.49.0}/gea/mqtt_port.py +0 -0
  247. {gea_program-0.47.0 → gea_program-0.49.0}/gea/op_seismicity.py +0 -0
  248. {gea_program-0.47.0 → gea_program-0.49.0}/gea/opcua_port.py +0 -0
  249. {gea_program-0.47.0 → gea_program-0.49.0}/gea/operator_app.py +0 -0
  250. {gea_program-0.47.0 → gea_program-0.49.0}/gea/osdu.py +0 -0
  251. {gea_program-0.47.0 → gea_program-0.49.0}/gea/patches.py +0 -0
  252. {gea_program-0.47.0 → gea_program-0.49.0}/gea/pdf_report.py +0 -0
  253. {gea_program-0.47.0 → gea_program-0.49.0}/gea/penetrator.py +0 -0
  254. {gea_program-0.47.0 → gea_program-0.49.0}/gea/permits.py +0 -0
  255. {gea_program-0.47.0 → gea_program-0.49.0}/gea/piwebapi.py +0 -0
  256. {gea_program-0.47.0 → gea_program-0.49.0}/gea/ports.py +0 -0
  257. {gea_program-0.47.0 → gea_program-0.49.0}/gea/ppdm.py +0 -0
  258. {gea_program-0.47.0 → gea_program-0.49.0}/gea/profile_catalog.py +0 -0
  259. {gea_program-0.47.0 → gea_program-0.49.0}/gea/project.py +0 -0
  260. {gea_program-0.47.0 → gea_program-0.49.0}/gea/qt6_downhole_app.py +0 -0
  261. {gea_program-0.47.0 → gea_program-0.49.0}/gea/quakeml.py +0 -0
  262. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reconciler.py +0 -0
  263. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  264. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  265. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  266. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/eia_prices.provenance.json +0 -0
  267. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  268. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  269. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  270. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  271. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  272. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/mahoning_example.json +0 -0
  273. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/mud_motor_specs.json +0 -0
  274. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/penetrator_ratings.json +0 -0
  275. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/seismicity_protocols.json +0 -0
  276. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/tool_sources.json +0 -0
  277. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/us_outline.json +0 -0
  278. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/us_outline.provenance.json +0 -0
  279. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_3d.provenance.json +0 -0
  280. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  281. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
  282. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
  283. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_surfaces.json.gz +0 -0
  284. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  285. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  286. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  287. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  288. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  289. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  290. {gea_program-0.47.0 → gea_program-0.49.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  291. {gea_program-0.47.0 → gea_program-0.49.0}/gea/report_history.py +0 -0
  292. {gea_program-0.47.0 → gea_program-0.49.0}/gea/rock_inventory.py +0 -0
  293. {gea_program-0.47.0 → gea_program-0.49.0}/gea/sample_record.py +0 -0
  294. {gea_program-0.47.0 → gea_program-0.49.0}/gea/sample_well_profile.csv +0 -0
  295. {gea_program-0.47.0 → gea_program-0.49.0}/gea/sbom.py +0 -0
  296. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seedlink.py +0 -0
  297. {gea_program-0.47.0 → gea_program-0.49.0}/gea/segy.py +0 -0
  298. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic.py +0 -0
  299. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_array.py +0 -0
  300. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_assoc.py +0 -0
  301. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_detect.py +0 -0
  302. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_film.py +0 -0
  303. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_harmonic.py +0 -0
  304. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_qc.py +0 -0
  305. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_response.py +0 -0
  306. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_signature.py +0 -0
  307. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_track.py +0 -0
  308. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_uncertainty.py +0 -0
  309. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_unlisted.py +0 -0
  310. {gea_program-0.47.0 → gea_program-0.49.0}/gea/seismic_watch.py +0 -0
  311. {gea_program-0.47.0 → gea_program-0.49.0}/gea/sensor_swap.py +0 -0
  312. {gea_program-0.47.0 → gea_program-0.49.0}/gea/series_cache.py +0 -0
  313. {gea_program-0.47.0 → gea_program-0.49.0}/gea/service_life.py +0 -0
  314. {gea_program-0.47.0 → gea_program-0.49.0}/gea/setup_checklist.py +0 -0
  315. {gea_program-0.47.0 → gea_program-0.49.0}/gea/shapefile.py +0 -0
  316. {gea_program-0.47.0 → gea_program-0.49.0}/gea/shell.py +0 -0
  317. {gea_program-0.47.0 → gea_program-0.49.0}/gea/shut_in.py +0 -0
  318. {gea_program-0.47.0 → gea_program-0.49.0}/gea/simulation.py +0 -0
  319. {gea_program-0.47.0 → gea_program-0.49.0}/gea/site_map.py +0 -0
  320. {gea_program-0.47.0 → gea_program-0.49.0}/gea/sla_report.py +0 -0
  321. {gea_program-0.47.0 → gea_program-0.49.0}/gea/slip.py +0 -0
  322. {gea_program-0.47.0 → gea_program-0.49.0}/gea/soak.py +0 -0
  323. {gea_program-0.47.0 → gea_program-0.49.0}/gea/sra.py +0 -0
  324. {gea_program-0.47.0 → gea_program-0.49.0}/gea/store_forward.py +0 -0
  325. {gea_program-0.47.0 → gea_program-0.49.0}/gea/strata_join.py +0 -0
  326. {gea_program-0.47.0 → gea_program-0.49.0}/gea/structure.py +0 -0
  327. {gea_program-0.47.0 → gea_program-0.49.0}/gea/supervisor.py +0 -0
  328. {gea_program-0.47.0 → gea_program-0.49.0}/gea/survey_cmd.py +0 -0
  329. {gea_program-0.47.0 → gea_program-0.49.0}/gea/survey_view.py +0 -0
  330. {gea_program-0.47.0 → gea_program-0.49.0}/gea/telemetry.py +0 -0
  331. {gea_program-0.47.0 → gea_program-0.49.0}/gea/tool_catalog.py +0 -0
  332. {gea_program-0.47.0 → gea_program-0.49.0}/gea/tool_library.py +0 -0
  333. {gea_program-0.47.0 → gea_program-0.49.0}/gea/tool_taxonomy.py +0 -0
  334. {gea_program-0.47.0 → gea_program-0.49.0}/gea/transient.py +0 -0
  335. {gea_program-0.47.0 → gea_program-0.49.0}/gea/vibration.py +0 -0
  336. {gea_program-0.47.0 → gea_program-0.49.0}/gea/well3d.py +0 -0
  337. {gea_program-0.47.0 → gea_program-0.49.0}/gea/well_assembler.py +0 -0
  338. {gea_program-0.47.0 → gea_program-0.49.0}/gea/well_report.py +0 -0
  339. {gea_program-0.47.0 → gea_program-0.49.0}/gea/well_test_validation.py +0 -0
  340. {gea_program-0.47.0 → gea_program-0.49.0}/gea/wits0.py +0 -0
  341. {gea_program-0.47.0 → gea_program-0.49.0}/gea/witsml.py +0 -0
  342. {gea_program-0.47.0 → gea_program-0.49.0}/gea/workspace.py +0 -0
  343. {gea_program-0.47.0 → gea_program-0.49.0}/gea_program.egg-info/dependency_links.txt +0 -0
  344. {gea_program-0.47.0 → gea_program-0.49.0}/gea_program.egg-info/entry_points.txt +0 -0
  345. {gea_program-0.47.0 → gea_program-0.49.0}/gea_program.egg-info/requires.txt +0 -0
  346. {gea_program-0.47.0 → gea_program-0.49.0}/gea_program.egg-info/top_level.txt +0 -0
  347. {gea_program-0.47.0 → gea_program-0.49.0}/setup.cfg +0 -0
  348. {gea_program-0.47.0 → gea_program-0.49.0}/tests/test_acceptance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gea-program
3
- Version: 0.47.0
3
+ Version: 0.49.0
4
4
  Summary: GEA-Program: downhole gauge monitoring - canonical measurement records, quality rules, drift evaluation with a re-fit change log, well-test validation, alarm management, model cards, store-and-forward, monthly SLA measurement, FAT/SAT protocols and a dashboard, generated from live objects in the client's reporting outline.
5
5
  Author: Daniel T. Murphy
6
6
  License: MPL-2.0
@@ -648,6 +648,35 @@ against it; `gea seedlink --config ... --hello` prints what a server says of
648
648
  itself; `gea help seedlink`. A TexNet station is served by EarthScope at
649
649
  `rtserve.iris.washington.edu:18000`, network `TX`.
650
650
 
651
+ ### Well collision checks
652
+
653
+ Every well scanned against every other: at each point of one well, the
654
+ closest point of the other, and the ISCWSA separation factor - the distance
655
+ between them less the holes and a surface margin, over k times the
656
+ uncertainty of that distance. Each well's position uncertainty comes from
657
+ the ISCWSA MWD Rev 4 error model (27 error sources, propagated as the model
658
+ says, the errors two wells share taken out of the distance between them),
659
+ checked against all 112 rows of the ISCWSA's own example; the Earth's field
660
+ at each well comes from the World Magnetic Model 2025. Each pair is levelled
661
+ as the ISCWSA's separation rule does - reported below 4.0, the approach
662
+ limit below 1.5, revise the plan below 1.25, below 1.0 only by the
663
+ operator's dispensation - with a company standard's k and margin kept with
664
+ its source. The Volve template's three wells are the worked example. The
665
+ **Collision** tab, `gea collision`, `gea help collision`.
666
+
667
+ ### One record, tied together
668
+
669
+ The invoice check, rig time, the daily summary, the alerts, the signed
670
+ dossier and the declarations sheet now read each other: an invoice that
671
+ differs is an alert; the morning summary carries each well's invoice check
672
+ and yesterday's rig time; a signed dossier carries the contract, the
673
+ invoices and daily reports as attached, the rig-time targets and casing,
674
+ and both checks as they read at signing. The declarations sheet declares
675
+ the contract, the rig-time targets, block weights, casing shoes and the
676
+ watch and summary settings, and every file the program reads as a table -
677
+ the sheet, an invoice, a daily report - may be an Excel workbook. Casing
678
+ shoes split rig time by hole section.
679
+
651
680
  ### Rig time
652
681
 
653
682
  What the rig did each second of the drilling record, read from the hook
@@ -609,6 +609,35 @@ against it; `gea seedlink --config ... --hello` prints what a server says of
609
609
  itself; `gea help seedlink`. A TexNet station is served by EarthScope at
610
610
  `rtserve.iris.washington.edu:18000`, network `TX`.
611
611
 
612
+ ### Well collision checks
613
+
614
+ Every well scanned against every other: at each point of one well, the
615
+ closest point of the other, and the ISCWSA separation factor - the distance
616
+ between them less the holes and a surface margin, over k times the
617
+ uncertainty of that distance. Each well's position uncertainty comes from
618
+ the ISCWSA MWD Rev 4 error model (27 error sources, propagated as the model
619
+ says, the errors two wells share taken out of the distance between them),
620
+ checked against all 112 rows of the ISCWSA's own example; the Earth's field
621
+ at each well comes from the World Magnetic Model 2025. Each pair is levelled
622
+ as the ISCWSA's separation rule does - reported below 4.0, the approach
623
+ limit below 1.5, revise the plan below 1.25, below 1.0 only by the
624
+ operator's dispensation - with a company standard's k and margin kept with
625
+ its source. The Volve template's three wells are the worked example. The
626
+ **Collision** tab, `gea collision`, `gea help collision`.
627
+
628
+ ### One record, tied together
629
+
630
+ The invoice check, rig time, the daily summary, the alerts, the signed
631
+ dossier and the declarations sheet now read each other: an invoice that
632
+ differs is an alert; the morning summary carries each well's invoice check
633
+ and yesterday's rig time; a signed dossier carries the contract, the
634
+ invoices and daily reports as attached, the rig-time targets and casing,
635
+ and both checks as they read at signing. The declarations sheet declares
636
+ the contract, the rig-time targets, block weights, casing shoes and the
637
+ watch and summary settings, and every file the program reads as a table -
638
+ the sheet, an invoice, a daily report - may be an Excel workbook. Casing
639
+ shoes split rig time by hole section.
640
+
612
641
  ### Rig time
613
642
 
614
643
  What the rig did each second of the drilling record, read from the hook
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.47.0"
126
+ __version__ = "0.49.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -590,6 +590,20 @@ def main(argv=None) -> int:
590
590
  p_dz.add_argument("--actor", type=str, default="cli")
591
591
  p_dz.add_argument("--json", action="store_true")
592
592
 
593
+ p_co = sub.add_parser("collision", help="every well scanned against every other: the closest approach and the ISCWSA separation factor, each well's position uncertainty by the ISCWSA MWD model and the field by WMM2025")
594
+ p_co.add_argument("--workspace", type=str, default=None)
595
+ p_co.add_argument("--scene", type=str, default=None, help="workspace (your wells) or volve (the worked example)")
596
+ p_co.add_argument("--write", action="store_true", help="write the client report")
597
+ p_co.add_argument("--k", type=str, default=None, help="declare the rule's scaling factor")
598
+ p_co.add_argument("--surface-margin", type=str, default=None, help="declare the surface margin, m")
599
+ p_co.add_argument("--sigma-pa", type=str, default=None, help="declare the project-ahead uncertainty, m")
600
+ p_co.add_argument("--source", type=str, default="", help="where the rule's values came from")
601
+ p_co.add_argument("--well", type=str, default=None)
602
+ p_co.add_argument("--hole", type=str, default=None, help="with --well: the hole size in inches ('' removes it)")
603
+ p_co.add_argument("--field", type=str, default=None, help="the magnetic field at LAT,LON[,YYYY-MM-DD] by WMM2025, and nothing else")
604
+ p_co.add_argument("--actor", type=str, default="cli")
605
+ p_co.add_argument("--json", action="store_true")
606
+
593
607
  p_rt = sub.add_parser("rig-time", help="what the rig did each second of the drilling record - drilling, connections, trips, the slips - and the time lost against the pad's own best")
594
608
  p_rt.add_argument("--workspace", type=str, required=True)
595
609
  p_rt.add_argument("--well", type=str, default=None, help="with --write: one well's report (without it, the pad's)")
@@ -599,6 +613,9 @@ def main(argv=None) -> int:
599
613
  p_rt.add_argument("--trip-out", type=str, default=None, help="declare the tripping-out target, ft/h")
600
614
  p_rt.add_argument("--source", type=str, default="", help="where the targets came from")
601
615
  p_rt.add_argument("--block-weight", type=str, default=None, help="with --well: the block weight in klbf where the estimate is wrong ('' removes it)")
616
+ p_rt.add_argument("--casing", type=str, default=None, help="with --well, --shoe-md and --source: declare a casing shoe, its size in inches as written (13-3/8)")
617
+ p_rt.add_argument("--shoe-md", type=str, default=None, help="the shoe's measured depth, ft")
618
+ p_rt.add_argument("--set-time", type=str, default=None, help="when the casing was set (the recorder's clock); otherwise when the hole is 50 ft past the shoe")
602
619
  p_rt.add_argument("--actor", type=str, default="cli")
603
620
  p_rt.add_argument("--json", action="store_true")
604
621
 
@@ -2205,9 +2222,10 @@ def main(argv=None) -> int:
2205
2222
  if not a.file:
2206
2223
  raise SystemExit("declare: give --file <sheet.csv> (or --template <out.csv> to start one)")
2207
2224
  try:
2208
- with open(a.file, encoding="utf-8-sig") as f:
2209
- text_ = f.read()
2210
- except OSError as e:
2225
+ from . import xlsx_read as _XR
2226
+ with open(a.file, "rb") as f:
2227
+ text_ = _XR.text_of(f.read())
2228
+ except (OSError, ValueError, KeyError) as e:
2211
2229
  raise SystemExit(f"declare: {e}")
2212
2230
  p = _DC.apply(ws, text_, a.actor, os.path.dirname(os.path.abspath(a.file)), write=a.apply)
2213
2231
  print(_json.dumps(p, default=str) if a.json else _DC.text(p))
@@ -2368,6 +2386,42 @@ def main(argv=None) -> int:
2368
2386
  print(f" {os.path.join(ws.reports_dir, e['path'])}")
2369
2387
  print(f" key fingerprint {e['fingerprint']}")
2370
2388
  return 0
2389
+ elif a.cmd == "collision":
2390
+ import json as _json
2391
+ from . import collision as _CO
2392
+ from .cli import _safe_stdout
2393
+ from .workspace import Workspace, WorkspaceError
2394
+ _safe_stdout()
2395
+ try:
2396
+ if a.field:
2397
+ from . import geomag as _GM
2398
+ parts = [x.strip() for x in a.field.split(",")]
2399
+ f_ = _GM.field(float(parts[0]), float(parts[1]), 0.0, parts[2] if len(parts) > 2 else None)
2400
+ print(f"{f_['model']} at {float(parts[0]):.4f}, {float(parts[1]):.4f} for {f_['decimal_year']:.2f}: declination {f_['declination_deg']:+.3f} deg, "
2401
+ f"dip {f_['dip_deg']:.3f} deg, total {f_['total_nT']:,.1f} nT" + (f" ({f_['note']})" if f_['note'] else ""))
2402
+ return 0
2403
+ ws = Workspace(a.workspace) if a.workspace else None
2404
+ if ws is not None and (a.k is not None or a.surface_margin is not None or a.sigma_pa is not None or a.hole is not None):
2405
+ if a.hole is not None and not a.well:
2406
+ raise ValueError("--hole needs --well")
2407
+ s_ = _CO.set_settings(ws, a.actor, a.k, a.surface_margin, a.sigma_pa, (a.well, a.hole) if a.hole is not None else None, a.source)
2408
+ print(f"rule: k {s_['k']:g}, surface margin {s_['surface_margin_m']:g} m, project-ahead {s_['sigma_pa_m']:g} m; holes: "
2409
+ + (", ".join(f"{k} {v:g} in" for k, v in s_['holes_in'].items()) or "8.5 in by default"))
2410
+ return 0
2411
+ scene = a.scene or ("workspace" if ws is not None else "volve")
2412
+ if a.write:
2413
+ if ws is None:
2414
+ raise ValueError("--write needs --workspace")
2415
+ r_ = _CO.write_report(ws, __version__, a.actor, scene)
2416
+ print(f"wrote {r_['report_id']}: {r_['paths']['html']}")
2417
+ if r_['paths'].get('pdf'):
2418
+ print(f" printable: {r_['paths']['pdf']}")
2419
+ return 0
2420
+ r = _CO.page(ws, scene)
2421
+ except (KeyError, ValueError, OSError, WorkspaceError) as e:
2422
+ raise SystemExit(f"collision: {e.args[0] if e.args else e}")
2423
+ print(_json.dumps({k: v for k, v in r.items()}, default=str) if a.json else _CO.text(r))
2424
+ return 0
2371
2425
  elif a.cmd == "rig-time":
2372
2426
  import json as _json
2373
2427
  from . import rig_state as _RS
@@ -2376,6 +2430,12 @@ def main(argv=None) -> int:
2376
2430
  _safe_stdout()
2377
2431
  try:
2378
2432
  ws = Workspace(a.workspace)
2433
+ if a.casing:
2434
+ if not a.well:
2435
+ raise ValueError("--casing needs --well")
2436
+ c_ = _RS.set_casing(ws, a.actor, a.well, a.casing, a.shoe_md, a.source, a.set_time)
2437
+ print(f"casing {c_['size']} in, shoe at {c_['shoe_md_ft']:,.0f} ft MD ({c_['source']})")
2438
+ return 0
2379
2439
  if a.w2w_min is not None or a.trip_in is not None or a.trip_out is not None or a.block_weight is not None:
2380
2440
  if a.block_weight is not None and not a.well:
2381
2441
  raise ValueError("--block-weight needs --well")
@@ -10913,6 +10913,389 @@ def section_cp_rig_time(tmp: str) -> None:
10913
10913
  + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
10914
10914
 
10915
10915
 
10916
+ def section_cq_tied_together(tmp: str) -> None:
10917
+ """Section CQ - the recent parts tied together: an Excel workbook read without Excel (shared and inline strings,
10918
+ numbers, booleans, dates and times from their cell formats, the 1904 epoch, a named sheet) and taken wherever a
10919
+ CSV is - the invoice check's files and the declarations sheet; the declarations sheet's new tables (the contract,
10920
+ the rig-time targets, block weights, casing, the watch and summary settings) planned, written, round-tripped and
10921
+ refused when wrong; rig time by hole section from declared casing; the invoice differing raised as an alert and
10922
+ cleared; the daily summary carrying each invoice check and yesterday's rig time; a signed dossier carrying the
10923
+ contract, the files, the targets, the casing and both checks as they read at signing; the API, the CLI, the tabs
10924
+ and the help pages."""
10925
+ import http.cookiejar
10926
+ import subprocess as _sp
10927
+ import urllib.request
10928
+ import urllib.error
10929
+ import base64 as _b64
10930
+ import zipfile as _zf
10931
+ from datetime import datetime as _dtm, timedelta as _td, timezone as _tz
10932
+ from . import __version__
10933
+ from . import alerts as AL
10934
+ from . import declarations as DC
10935
+ from . import dossier as DZ
10936
+ from . import helplib as H
10937
+ from . import invoice_check as IC
10938
+ from . import pad_summary as PS
10939
+ from . import rig_state as RS
10940
+ from . import seismic_watch as SW
10941
+ from . import xlsx_read as XR
10942
+ from .workspace import Workspace
10943
+ from .service import Service, Users
10944
+ pkg = Path(__file__).parent
10945
+ d = Path(tmp, "cq"); d.mkdir()
10946
+
10947
+ # -- CQ1 the workbook -------------------------------------------------------------------------------------------------
10948
+ XR.write_simple(str(d / "a.xlsx"), [["Date", "Rate Type", "Quantity", "Note"], [_dtm(2026, 7, 1), "Operating day rate", 1, 'with, a "comma"'],
10949
+ [_dtm(2026, 7, 2, 6, 30), "Standby", 12.5, None]])
10950
+ a = XR.rows((d / "a.xlsx").read_bytes())
10951
+ ns = 'xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"'
10952
+ with _zf.ZipFile(d / "b.xlsx", "w") as z: # a hand-made workbook: two sheets, inline strings, booleans, the 1904 epoch, a custom date format
10953
+ z.writestr("xl/workbook.xml", f'<workbook {ns}><workbookPr date1904="1"/><sheets><sheet name="Notes" sheetId="1" r:id="rId1"/><sheet name="Invoice" sheetId="2" r:id="rId2"/></sheets></workbook>')
10954
+ z.writestr("xl/_rels/workbook.xml.rels", '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="worksheets/sheet1.xml" Type="x"/><Relationship Id="rId2" Target="/xl/worksheets/sheet2.xml" Type="x"/></Relationships>')
10955
+ z.writestr("xl/styles.xml", f'<styleSheet {ns}><numFmts count="1"><numFmt numFmtId="164" formatCode="dd/mm/yyyy\\ hh:mm"/><numFmt numFmtId="165" formatCode="#,##0.00"/></numFmts>'
10956
+ '<cellXfs count="3"><xf numFmtId="0"/><xf numFmtId="164"/><xf numFmtId="165"/></cellXfs></styleSheet>')
10957
+ z.writestr("xl/worksheets/sheet1.xml", f'<worksheet {ns}><sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>notes</t></is></c></row></sheetData></worksheet>')
10958
+ z.writestr("xl/worksheets/sheet2.xml", f'<worksheet {ns}><sheetData><row r="1"><c r="A1" t="inlineStr"><is><t>When</t></is></c><c r="C1" t="inlineStr"><is><t>Paid</t></is></c></row>'
10959
+ '<row r="3"><c r="A3" s="1"><v>44743.25</v></c><c r="B3" s="2"><v>1234.5</v></c><c r="C3" t="b"><v>1</v></c></row></sheetData></worksheet>')
10960
+ bx = (d / "b.xlsx").read_bytes()
10961
+ b2 = XR.rows(bx, "Invoice")
10962
+ c1 = {
10963
+ "simple": a == [["Date", "Rate Type", "Quantity", "Note"], ["2026-07-01", "Operating day rate", "1", 'with, a "comma"'], ["2026-07-02T06:30", "Standby", "12.5"]],
10964
+ "csv": XR.to_csv((d / "a.xlsx").read_bytes()).splitlines()[1] == '2026-07-01,Operating day rate,1,"with, a ""comma"""',
10965
+ "sheets": XR.sheets(bx) == ["Notes", "Invoice"] and XR.rows(bx) == [["notes"]],
10966
+ "hand_made": b2 == [["When", "", "Paid"], [], ["2026-07-02T06:00", "1234.5", "TRUE"]],
10967
+ "text_of": XR.text_of(b"a,b\n1,2\n") == "a,b\n1,2\n" and not XR.is_xlsx(b"PK not a zip"),
10968
+ }
10969
+ try:
10970
+ XR.rows(bx, "Nope"); c1["named"] = False
10971
+ except ValueError as e:
10972
+ c1["named"] = "Notes, Invoice" in str(e)
10973
+ failed1 = [k for k, v in c1.items() if not v]
10974
+ ok(not failed1,
10975
+ "CQ1 an Excel workbook read with the standard library: shared and inline strings, a comma and quotes kept, whole numbers without "
10976
+ "'.0', dates and date-times from built-in and custom formats, the 1904 epoch, booleans, a gap row kept as a gap, the first or a "
10977
+ "named sheet; CSV text passed through" + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
10978
+
10979
+ # -- CQ2 the declarations sheet, casing, and the invoice as a workbook --------------------------------------------------
10980
+ F = RS.selftest_edr(str(d))
10981
+ G = IC.selftest_files(str(d))
10982
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad CQ", actor="tester")
10983
+ w1 = ws.add_well_file(F["path"], display="RIG-1", actor="tester"); w2 = ws.add_well_file(F["path"], display="RIG-2", actor="tester")
10984
+ wi = ws.add_well_file(G["edr"], display="INV-1", actor="tester")
10985
+ sheet = ("[contract],day_start,report_date,operating,standby,repair,repair_hours_per_month,source\n,06:00,start,30000,25000,0,2,daywork contract A\n"
10986
+ "[rig_time],w2w_min,trip_in_ft_h,source\n,,2800,program 2026\n[block_weights],well,block_weight_klbf\n,RIG-2,42\n"
10987
+ "[casing],well,size_in,shoe_md_ft,set,source\n,RIG-1,9-5/8,10150,,tally\n,RIG-2,9-5/8,10150,,tally\n,RIG-1,13-3/8,2000,,tally\n"
10988
+ "[settings],setting,value\n,watch_every_min,10\n,summary_at,10:45\n,summary_send,no\n")
10989
+ rows_ = [ln.split(",") for ln in sheet.strip().splitlines()]
10990
+ XR.write_simple(str(d / "decl.xlsx"), rows_)
10991
+ p_x = DC.plan(ws, XR.text_of((d / "decl.xlsx").read_bytes()))
10992
+ p_w = DC.apply(ws, sheet, "tester", write=True)
10993
+ p_rt = DC.plan(ws, DC.template(ws))
10994
+ p_bad = DC.plan(ws, "[contract],operating,source\n,30000,\n[settings],setting,value\n,watch_mode,sometimes\n,colour,blue\n[casing],well,size_in,shoe_md_ft,source\n,RIG-1,9 5/8,100,x\n,NOPE,7,100,x\n")
10995
+ pad = RS.pad(ws)
10996
+ W1 = next(w for w in pad["wells"] if w["display"] == "RIG-1")
10997
+ secs = [c["section"] for c in W1["connection_list"]]
10998
+ XR.write_simple(str(d / "inv.xlsx"), [ln.split(",") if i == 0 else [(_dtm.fromisoformat(x) if j == 0 else (float(x) if re.fullmatch(r"[\d.]+", x) else x)) for j, x in enumerate(ln.split(","))]
10999
+ for i, ln in enumerate(G["invoice"].strip().splitlines())])
11000
+ IC.attach(ws, "tester", wi["id"], "ddr", "ddr.csv", G["ddr"].encode(), "reports")
11001
+ IC.attach(ws, "tester", wi["id"], "invoice", "inv.xlsx", (d / "inv.xlsx").read_bytes(), "invoice as a workbook")
11002
+ ic = IC.check(ws, wi["id"])
11003
+ c2 = {
11004
+ "xlsx_sheet": p_x["counts"]["NEW"] == 6 and p_x["counts"]["CHANGE"] == 3 and not p_x["errors"],
11005
+ "written": p_w["applied"] and IC.terms(ws)["rates_usd_per_day"] == {"operating": 30000.0, "standby": 25000.0, "repair": 0.0}
11006
+ and RS.settings(ws)["targets"] == {"trip_in_ft_h": 2800.0} and RS.settings(ws)["block_weight_klbf"] == {w2["id"]: 42.0}
11007
+ and [x["size"] for x in RS.casing(ws)[w1["id"]]] == ["13-3/8", "9-5/8"] and SW.settings(ws)["every_min"] == 10
11008
+ and PS.settings(ws)["at"] == "10:45" and PS.settings(ws)["send"] is False,
11009
+ "round_trip": p_rt["counts"]["NEW"] == 0 and p_rt["counts"]["CHANGE"] == 0 and not p_rt["errors"],
11010
+ "refused": len(p_bad["errors"]) == 5 and any("watch_mode" in e for e in p_bad["errors"]) and any("no setting 'colour'" in e for e in p_bad["errors"])
11011
+ and any("13-3/8, 9-5/8 or 7" in e for e in p_bad["errors"]) and any("came from" in e for e in p_bad["errors"]),
11012
+ "sections": secs == ["below the 13-3/8 in shoe", "below the 9-5/8 in shoe", "below the 9-5/8 in shoe"]
11013
+ and all(x["hole"] == "CASED" for x in W1["trips"]) and set(pad["targets"]["by_section"]) == {"below the 9-5/8 in shoe"}
11014
+ and next(w for w in pad["wells"] if w["display"] == "RIG-2")["block_weight_klbf"] == 42.0,
11015
+ "invoice_xlsx": ic["status"] == "DIFFERS" and ic["totals"]["usd_in_question"] == 37666.67,
11016
+ }
11017
+ failed2 = [k for k, v in c2.items() if not v]
11018
+ ok(not failed2,
11019
+ "CQ2 the declarations sheet's new tables - the contract, the rig-time targets, a block weight, casing shoes, the watch and summary "
11020
+ "settings - read from a workbook, written, round-tripped to nothing to do, and refused when wrong (no source, an unknown setting, a "
11021
+ "size not in inches as written, a well not there); connections by hole section and trips inside casing; an invoice attached as a "
11022
+ "workbook checked to the same 37,666.67" + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
11023
+
11024
+ # -- CQ3 the alert, the summary, the dossier ----------------------------------------------------------------------
11025
+ now = _dtm(2026, 7, 2, 12, 0, tzinfo=_tz.utc)
11026
+ k1 = AL.check_conditions(ws, None, now, force=True)
11027
+ st1 = dict(AL.state(ws)["conditions"])
11028
+ g = PS.gather(ws, _dtm(2026, 8, 2, 7, 0, tzinfo=_tz.utc), _dtm(2026, 8, 1, 0, 0, tzinfo=_tz.utc))
11029
+ RS.write_report(ws, __version__, "tester")
11030
+ e1 = DZ.build(ws, "tester", __version__, None)
11031
+ with _zf.ZipFile(os.path.join(ws.reports_dir, e1["path"])) as z:
11032
+ names = set(z.namelist())
11033
+ rt = json.loads(z.read("results/rig_time.json"))
11034
+ icj = json.loads(z.read(f"results/invoice_check/{wi['id']}.json"))
11035
+ v1 = DZ.verify(os.path.join(ws.reports_dir, e1["path"]), ws)
11036
+ for f_ in IC.files(ws, wi["id"]): # the corrected report and invoice replace them
11037
+ IC.remove(ws, "tester", f_["file"])
11038
+ ddr_ok = G["ddr"].replace("2026-07-02,14:00,16:00,2,6,Trip out", "2026-07-02,14:00,16:00,2,7,Service rig").replace("6200,7600", "6200,7400")
11039
+ inv_ok = ("Date,Rate Type,Quantity,Unit,Unit Price,Amount\n2026-07-01,Operating day rate,1,day,30000,30000\n"
11040
+ "2026-07-02,Operating day rate,22,hour,1250,27500\n2026-07-02,Repair rate,2,hour,0,0\n"
11041
+ "2026-07-03,Standby rate,20,hour,1041.6667,20833.33\n2026-07-03,Operating day rate,4,hour,1250,5000\n2026-07-04,Operating day rate,1,day,30000,30000\n")
11042
+ IC.attach(ws, "tester", wi["id"], "ddr", "ddr_ok.csv", ddr_ok.encode(), "corrected reports")
11043
+ IC.attach(ws, "tester", wi["id"], "invoice", "inv_ok.csv", inv_ok.encode(), "corrected invoice")
11044
+ k2 = AL.check_conditions(ws, None, now + _td(minutes=1))
11045
+ c3 = {
11046
+ "alert": [a_["event"] for a_ in k1 if a_["event"] == "invoice.differs"] == ["invoice.differs"] and f"invoice.differs:{wi['id']}" in st1
11047
+ and "37,666.67" in next(a_ for a_ in k1 if a_["event"] == "invoice.differs")["detail"],
11048
+ "cleared": [a_["event"] for a_ in k2] == ["condition.cleared"] and k2[0]["cleared"] == "invoice.differs",
11049
+ "summary": any(x["item"] == "the invoice check of INV-1" and x["status"] == "DIFFERS" for x in g["due"])
11050
+ and [x["well"] for x in g["rig_time"]] == ["RIG-1", "RIG-2"] and all(x["connections"] == 3 and x["date"] == "2026-08-01" for x in g["rig_time"])
11051
+ and all(x["lost_usd"] is not None for x in g["rig_time"]),
11052
+ "dossier": v1["status"] == "VALID" and {"declarations/economics/invoices/terms.json", "declarations/economics/invoices/files.json",
11053
+ "declarations/drillstring/rig_time.json", "declarations/drillstring/casing.json"} <= names
11054
+ and any(n.startswith(f"declarations/economics/invoices/{wi['id']}/") and n.endswith("_inv.xlsx") for n in names)
11055
+ and icj["status"] == "DIFFERS" and len(rt["wells"]) == 3 and rt["targets"]["trip_in_ft_h"]["value"] == 2800,
11056
+ }
11057
+ failed3 = [k for k, v in c3.items() if not v]
11058
+ ok(not failed3,
11059
+ "CQ3 an invoice that differs raised as an alert with its dollars, and cleared when a corrected invoice replaces it; the daily summary "
11060
+ "carrying the invoice check and yesterday's rig time well by well with its cost at the contract's rate; a signed pad dossier carrying "
11061
+ "the contract, the invoice files as attached, the targets and the casing, and both checks as they read at signing - VALID"
11062
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
11063
+
11064
+ # -- CQ4 the API, the CLI, the tabs, the help ------------------------------------------------------------------------
11065
+ Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer"); Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator")
11066
+ svc = Service(wsp, port=0, scheduler=False).start()
11067
+ base_url = svc.url.rstrip("/")
11068
+
11069
+ def opener(name, pw_):
11070
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
11071
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw_}).encode())
11072
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
11073
+ return o
11074
+
11075
+ def call(o, method, path, body=None):
11076
+ rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
11077
+ if method == "POST":
11078
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
11079
+ try:
11080
+ with o.open(rq, timeout=120) as rr_:
11081
+ return rr_.status, json.loads(rr_.read())
11082
+ except urllib.error.HTTPError as e:
11083
+ return e.code, json.loads(e.read())
11084
+ try:
11085
+ V, O = opener("v", "viewer-pass-1"), opener("op", "op-pass-123")
11086
+ s_vc, _ = call(V, "POST", "/api/rig-time/casing", {"well_id": w2["id"], "size": "7", "shoe_md_ft": 9000, "source": "x"})
11087
+ s_oc, ocj = call(O, "POST", "/api/rig-time/casing", {"well_id": w2["id"], "size": "7", "shoe_md_ft": 9000, "source": "tally"})
11088
+ s_ob, _ = call(O, "POST", "/api/rig-time/casing", {"well_id": w2["id"], "size": "seven", "shoe_md_ft": 9000, "source": "tally"})
11089
+ s_or, _ = call(O, "POST", "/api/rig-time/casing/remove", {"well_id": w2["id"], "size": "7"})
11090
+ s_dx, dxj = call(O, "POST", "/api/declare", {"content_b64": _b64.b64encode((d / "decl.xlsx").read_bytes()).decode()})
11091
+ s_ia, iaj = call(O, "POST", "/api/invoice-check/attach", {"well_id": wi["id"], "kind": "invoice", "filename": "inv2.xlsx", "source": "workbook",
11092
+ "content_b64": _b64.b64encode((d / "inv.xlsx").read_bytes()).decode()})
11093
+ s_al, alj = call(V, "GET", "/api/alerts")
11094
+ finally:
11095
+ svc.stop()
11096
+ cli_c = _sp.run([sys.executable, "-m", "gea", "rig-time", "--workspace", wsp, "--well", w2["id"], "--casing", "13-3/8", "--shoe-md", "2000", "--source", "tally"],
11097
+ capture_output=True, text=True, cwd=str(pkg.parent))
11098
+ cli_d = _sp.run([sys.executable, "-m", "gea", "declare", "--workspace", wsp, "--file", str(d / "decl.xlsx")], capture_output=True, text=True, cwd=str(pkg.parent))
11099
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
11100
+ c4 = {
11101
+ "api": s_vc == 403 and s_oc == 200 and ocj["shoe_md_ft"] == 9000 and s_ob == 400 and s_or == 200 and s_dx == 200 and dxj["counts"]["SAME"] == 9
11102
+ and s_ia == 200 and iaj["lines"] == 7 and s_al == 200 and "invoice.differs" in alj["events"],
11103
+ "cli": cli_c.returncode == 0 and "13-3/8 in, shoe at 2,000 ft" in cli_c.stdout and cli_d.returncode == 0 and "already declared" in cli_d.stdout,
11104
+ "page": all(x in page_src for x in ("rtC", "/api/rig-time/casing", ".xlsx", "content_b64: DC.b64", "'invoice.differs': 'Invoice differs'")),
11105
+ "help": all(not H.check(t_) for t_ in ("declare", "rig-time", "invoice-check", "dossier", "alerts", "pad-summary"))
11106
+ and "[casing]" in (pkg / "help" / "declare.md").read_text(encoding="utf-8") and "Hole sections" in (pkg / "help" / "rig-time.md").read_text(encoding="utf-8")
11107
+ and "invoice.differs" in (pkg / "help" / "alerts.md").read_text(encoding="utf-8") and "results/" in (pkg / "help" / "dossier.md").read_text(encoding="utf-8"),
11108
+ }
11109
+ failed4 = [k for k, v in c4.items() if not v]
11110
+ ok(not failed4,
11111
+ "CQ4 on the API (only an operator declares or removes a casing shoe, a size that is not one refused; a declarations workbook and an "
11112
+ "invoice workbook sent as they are; the new alert listed), the CLI (a casing shoe, a declarations workbook), the tabs and the help pages"
11113
+ + (f" [failed: {', '.join(failed4)}]" if failed4 else ""))
11114
+
11115
+
11116
+ def section_cr_collision(tmp: str) -> None:
11117
+ """Section CR - well collision checks: each well's position uncertainty by the ISCWSA MWD Rev 4 error model,
11118
+ checked against all 112 rows of the ISCWSA's own example; the Earth's field from WMM2025, checked against a
11119
+ published example; the ISCWSA separation factor, its arithmetic recomputed by hand with the errors two wells share
11120
+ taken out; the Volve template as the worked example; a workspace's wells checked, the rule and the hole sizes kept
11121
+ by an operator only; the API, the CLI, the report, the tab and the help."""
11122
+ import hashlib as _hl
11123
+ import http.cookiejar
11124
+ import math as _m
11125
+ import subprocess as _sp
11126
+ import time as _t
11127
+ import urllib.request
11128
+ import urllib.error
11129
+ import numpy as _np
11130
+ from . import collision as CO
11131
+ from . import edr as E
11132
+ from . import geomag as GM
11133
+ from . import helplib as H
11134
+ from . import pdf_report as PR
11135
+ from . import well3d as W
11136
+ from . import wellpos as WP
11137
+ from .workspace import Workspace
11138
+ from .service import Service, Users
11139
+ pkg = Path(__file__).parent
11140
+ d = Path(tmp, "collision"); d.mkdir()
11141
+
11142
+ # -- CR1 the error model and the field against their published examples --------------------------------------------
11143
+ ex = WP.check_example()
11144
+ tot8 = ex["totals"]["8000"]
11145
+ f1 = GM.field(42.283, -71.35, 1000.0, "2028-07-04")
11146
+ f2 = GM.field(31.42, -104.10, 0.0, 2031.5)
11147
+ c1 = {
11148
+ "example": ex["rows"] == 112 and ex["sources"] == 27 and ex["depths_m"] == [1200, 2100, 5100, 8000] and not ex["disagree"] and ex["worst_relative"] < 1e-3,
11149
+ "totals": abs(tot8[0] - 8547.78) < 0.05 and abs(tot8[1] - 712.69) < 0.05 and abs(tot8[2] - 468.13) < 0.05 and abs(tot8[3] + 2252.31) < 0.05,
11150
+ "attribution": "ISCWSA" in ex["source"] and "welleng" in ex["source"] and "Apache" in ex["license"],
11151
+ "wmm": abs(f1["declination_deg"] + 13.6891) < 5e-4 and abs(f1["dip_deg"] - 65.9899) < 5e-4 and abs(f1["total_nT"] - 50940) < 1.0
11152
+ and abs(f1["north_nT"] - 20139) < 1.0 and abs(f1["east_nT"] + 4905) < 1.0 and abs(f1["down_nT"] - 46532) < 1.0 and f1["in_range"],
11153
+ "wmm_range": not f2["in_range"] and "extrapolated" in f2["note"] and abs(GM.decimal_year("2026-07-02") - (2026 + 182 / 365)) < 1e-9,
11154
+ "coefficients": _hl.md5((pkg / "reference" / "WMM2025.COF").read_bytes()).hexdigest() == "a1b6402dd23658d15affb0f31babccc7",
11155
+ }
11156
+ failed1 = [k for k, v in c1.items() if not v]
11157
+ ok(not failed1,
11158
+ "CR1 each well's uncertainty by the ISCWSA MWD Rev 4 error model: all 112 rows of the ISCWSA's own example (27 error sources "
11159
+ "and the total, at 1,200, 2,100, 5,100 and 8,000 m MD) recomputed and agreeing to the printed digits, its source and licence "
11160
+ "carried; the field by WMM2025 agreeing with a published example (declination -13.6891, dip 65.9899 deg, 50,940 nT), a date "
11161
+ "past the model's years answered and called extrapolated, the coefficients as NOAA published them"
11162
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
11163
+
11164
+ # -- CR2 the separation factor's arithmetic ----------------------------------------------------------------------------
11165
+ fld = {"declination_deg": 6.0, "dip_deg": 60.0, "total_nT": 48000.0}
11166
+ md = [0.0, 500.0, 1000.0, 2000.0]
11167
+ mk = lambda i, x, inc=(0, 0, 0, 0), az=(0, 0, 0, 0), y=0.0: {"id": i, "display": i, "md": md, "inc": list(inc), "az": list(az), "x0": x, "y0": y,
11168
+ "ref_elev_m": 0.0, "field": fld, "hole_in": 8.5}
11169
+ rule = {**CO.DEFAULTS}
11170
+ A, B = CO.prepare(mk("A", 0.0)), CO.prepare(mk("B", 10.0))
11171
+ sAB, sBA = CO.scan(A, B, rule), CO.scan(B, A, rule)
11172
+ u = WP.well_uncertainty(_np.array(md), _np.zeros(4), _np.zeros(4), _np.array(md), 6.0, 60.0, 48000.0)
11173
+ Cee = float(u["cov"][-1][1, 1]); Gee = sum(float(g[-1][1]) ** 2 for g in u["global"].values())
11174
+ sig_hand = _m.sqrt(2.0 * (Cee - Gee))
11175
+ sf_hand = (10.0 - 2 * 8.5 * 0.0254 / 2 - 0.3) / (3.5 * _m.sqrt(sig_hand ** 2 + 0.25))
11176
+ i_td = len(sAB["profile"]["md_m"]) - 1
11177
+ E1, E2 = CO.prepare(mk("E1", 0.0, (0, 0, 30, 30), (90, 90, 90, 90))), CO.prepare(mk("E2", 0.0, (0, 0, 30, 30), (90, 90, 90, 90), 10.0))
11178
+ s_sh = CO.scan(E1, E2, rule) # parallel, inclined east, 10 m apart north-south: the declination moves both
11179
+ s_ng = CO.scan({**E1, "glob": {k: v * 0 for k, v in E1["glob"].items()}}, {**E2, "glob": {k: v * 0 for k, v in E2["glob"].items()}}, rule)
11180
+ j_td = len(s_sh["profile"]["md_m"]) - 1
11181
+ ue = WP.well_uncertainty(_np.array(md), _np.array([0.0, 0.0, 30.0, 30.0]), _np.full(4, 90.0), _np.array(W.Path(md, [0, 0, 30, 30], [90] * 4, 0.0, 0.0, 0.0, 0.0).tvd), 6.0, 60.0, 48000.0)
11182
+ Cnn = float(ue["cov"][-1][0, 0]); Gnn = sum(float(g[-1][0]) ** 2 for g in ue["global"].values())
11183
+ C_, D_ = CO.prepare(mk("C", 0.0, (0, 0, 30, 30), (0, 0, 90, 90))), CO.prepare(mk("D", 30.0, (0, 0, 30, 30), (0, 0, 270, 270)))
11184
+ sCD = CO.scan(C_, D_, rule)
11185
+ c2 = {
11186
+ "hand": abs(sAB["profile"]["sigma_m"][i_td] - sig_hand) < 2e-3 and abs(sAB["profile"]["sf"][i_td] - sf_hand) < 2e-3 and abs(sAB["profile"]["distance_m"][i_td] - 10.0) < 1e-6,
11187
+ "shared_out": set(u["global"]) == {"DECG", "DBHG", "DSTG"} and Gnn > 0.01 and abs(s_sh["profile"]["sigma_m"][j_td] - _m.sqrt(2.0 * (Cnn - Gnn))) < 2e-3
11188
+ and abs(s_ng["profile"]["sigma_m"][j_td] - _m.sqrt(2.0 * Cnn)) < 2e-3 and abs(s_sh["profile"]["distance_m"][j_td] - 10.0) < 1e-3,
11189
+ "symmetric": abs(sAB["min_sf"] - sBA["min_sf"]) < 1e-6 and sAB["at_md_m"] == 2000.0,
11190
+ "crossing": sCD["min_sf"] < 1.0 and sCD["level"] == "BELOW 1.0" and sCD["closest_m"] < 1.0 and sCD["sections"] and sCD["sections"][0]["from_md_m"] < sCD["at_md_m"],
11191
+ "levels": [CO.level(x)[0] for x in (0.99, 1.0, 1.2, 1.25, 1.4, 1.5, 3.99, 4.0)]
11192
+ == ["BELOW 1.0", "REVISE THE PLAN", "REVISE THE PLAN", "APPROACH LIMIT", "APPROACH LIMIT", "REPORTED", "REPORTED", "CLEAR"],
11193
+ }
11194
+ failed2 = [k for k, v in c2.items() if not v]
11195
+ ok(not failed2,
11196
+ "CR2 the separation factor's arithmetic: two vertical wells 10 m apart recomputed by hand at TD - sigma along the line between "
11197
+ "them from each well's covariance less the errors both share (declination, the global magnetic and depth terms), the holes' "
11198
+ "radii, the surface margin, project-ahead and k; leaving the shared errors in widens sigma; the same from either well; two wells "
11199
+ "steered at each other found below 1.0 where they cross; the four levels at their edges"
11200
+ + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
11201
+
11202
+ # -- CR3 the worked example: the Volve template ------------------------------------------------------------------------
11203
+ t0 = _t.time()
11204
+ v = CO.page(None, "volve")
11205
+ dt = _t.time() - t0
11206
+ w_ = v["worst"]; wl = {x["display"]: x for x in v["wells"]}
11207
+ c3 = {
11208
+ "wells": len(v["wells"]) == 3 and len(v["pairs"]) == 6 and abs(wl["15/9-F-10"]["td_md_m"] - 5300.1) < 0.1
11209
+ and all(abs(x["field"]["declination_deg"] - 1.12) < 0.05 for x in v["wells"]) and all("grid convergence" in x["azimuth_note"] for x in v["wells"]),
11210
+ "worst": w_["level"] == "BELOW 1.0" and abs(w_["min_sf"] - 0.29) < 0.01 and {w_["reference_name"], w_["offset_name"]} == {"15/9-F-5", "15/9-F-10"}
11211
+ and abs(w_["at_md_m"] - 250) < 15 and abs(w_["distance_m"] - 1.20) < 0.05,
11212
+ "order": [p["min_sf"] for p in v["pairs"]] == sorted(p["min_sf"] for p in v["pairs"]) and all(len(p["profile"]["md_m"]) == len(p["profile"]["sf"]) for p in v["pairs"]),
11213
+ "ellipses": wl["15/9-F-10"]["td_ellipse_1sigma"]["major_m"] > wl["15/9-F-5"]["td_ellipse_1sigma"]["major_m"] > wl["NO 15/9-F-4"]["td_ellipse_1sigma"]["major_m"] > 1.0,
11214
+ "fast": dt < 20.0,
11215
+ }
11216
+ failed3 = [k for k, v_ in c3.items() if not v_]
11217
+ ok(not failed3,
11218
+ "CR3 the worked example, the Volve template's three wells (Equinor's released data): every pair scanned both ways, the field from "
11219
+ "WMM2025 at the template and the grid convergence applied; the closest, 15/9-F-5 and 15/9-F-10 near the surface where the slots "
11220
+ "sit 1.2 m apart, below 1.0; the longest well the widest ellipse at TD"
11221
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
11222
+
11223
+ # -- CR4 a workspace's wells, the API, the CLI, the report, the tab and the help ----------------------------------------
11224
+ ev = E.selftest_events(str(d))
11225
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
11226
+ ids = []
11227
+ for i, n in enumerate(("Well A", "Well B", "Well C")):
11228
+ w = ws.add_well_file(ev["path"], display=n, actor="tester"); ids.append(w["id"])
11229
+ ws.set_well(w["id"], surface={"lat": 31.42, "lon": -104.10 + 0.0001 * i, "datum": "NAD83", "elevation": {"ft": 3000.0, "datum": "KB", "source": "test plat"}}, actor="tester")
11230
+ W.attach_trajectory(ws, ids[0], "a.csv", b"MD (ft),Inc (deg),Azi (deg)\n0,0,0\n1000,0,0\n2000,10,90\n3000,30,90\n4000,45,95\n5750,60,100\n", "tester", "true", "ft", None, None, None, "test survey")
11231
+ W.attach_trajectory(ws, ids[1], "b.csv", b"MD (ft),Inc (deg),Azi (deg)\n0,0,0\n1200,0,0\n2200,12,270\n3500,35,265\n5500,55,260\n", "tester", "true", "ft", None, None, None, "test survey")
11232
+ p0 = CO.page(ws)
11233
+ Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer"); Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator")
11234
+ svc = Service(wsp, port=0, scheduler=False).start()
11235
+ base_url = svc.url.rstrip("/")
11236
+
11237
+ def opener(name, pw_):
11238
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
11239
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw_}).encode())
11240
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
11241
+ return o
11242
+
11243
+ def call(o, method, path, body=None):
11244
+ rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
11245
+ if method == "POST":
11246
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
11247
+ try:
11248
+ with o.open(rq, timeout=120) as rr_:
11249
+ return rr_.status, json.loads(rr_.read())
11250
+ except urllib.error.HTTPError as e:
11251
+ return e.code, json.loads(e.read())
11252
+ try:
11253
+ V, O = opener("v", "viewer-pass-1"), opener("op", "op-pass-123")
11254
+ s_vg, vg = call(V, "GET", "/api/collision")
11255
+ s_vv, vv = call(V, "GET", "/api/collision?scene=volve")
11256
+ s_bad, _ = call(V, "GET", "/api/collision?scene=nowhere")
11257
+ s_vs, _ = call(V, "POST", "/api/collision/settings", {"k": 3.0, "source": "x"})
11258
+ s_ns, _ = call(O, "POST", "/api/collision/settings", {"k": 3.0})
11259
+ s_lk, _ = call(O, "POST", "/api/collision/settings", {"k": 0.1, "source": "standard"})
11260
+ s_ok, okj = call(O, "POST", "/api/collision/settings", {"k": 3.0, "surface_margin_m": 0.5, "source": "company anti-collision standard, rev 3"})
11261
+ s_hb, _ = call(O, "POST", "/api/collision/settings", {"well_id": ids[0], "hole_in": 99})
11262
+ s_ho, hoj = call(O, "POST", "/api/collision/settings", {"well_id": ids[0], "hole_in": 12.25})
11263
+ s_ag, ag = call(V, "GET", "/api/collision")
11264
+ s_vw, _ = call(V, "POST", "/api/collision/write", {})
11265
+ s_ow, owj = call(O, "POST", "/api/collision/write", {})
11266
+ finally:
11267
+ svc.stop()
11268
+ cli_v = _sp.run([sys.executable, "-m", "gea", "collision", "--scene", "volve"], capture_output=True, text=True, cwd=str(pkg.parent))
11269
+ cli_f = _sp.run([sys.executable, "-m", "gea", "collision", "--field", "42.283,-71.35,2028-07-04"], capture_output=True, text=True, cwd=str(pkg.parent))
11270
+ cli_w = _sp.run([sys.executable, "-m", "gea", "collision", "--workspace", wsp, "--write"], capture_output=True, text=True, cwd=str(pkg.parent))
11271
+ pdf = Path(wsp, "reports", "collision.pdf")
11272
+ rd = PR.read(pdf.read_bytes()) if pdf.exists() else {"ok": False, "text": []}
11273
+ audit = Path(ws.audit_path).read_text(encoding="utf-8")
11274
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
11275
+ wa = next((p for p in ag.get("pairs", []) if p["reference"] == ids[0]), {}) if s_ag == 200 else {}
11276
+ c4 = {
11277
+ "workspace": len(p0["wells"]) == 2 and len(p0["pairs"]) == 2 and [x["id"] for x in p0["left_out"]] == [ids[2]] and "survey" in p0["left_out"][0]["why"]
11278
+ and all("WMM2025 at the well" in x["field_note"] for x in p0["wells"]) and p0["worst"]["level"] == "BELOW 1.0",
11279
+ "api": s_vg == 200 and vg["scene"] == "workspace" and len(vg["pairs"]) == 2 and s_vv == 200 and len(vv["pairs"]) == 6 and s_bad in (400, 404)
11280
+ and s_vs == 403 and s_ns == 400 and s_lk == 400 and s_ok == 200 and okj["k"] == 3.0 and s_hb == 400 and s_ho == 200 and hoj["holes_in"][ids[0]] == 12.25
11281
+ and s_ag == 200 and ag["rule"]["k"] == 3.0 and ag["rule_source"].startswith("company") and wa.get("min_sf", 0) < p0["pairs"][0]["min_sf"] + 1
11282
+ and next(x for x in ag["wells"] if x["id"] == ids[0])["hole_in"] == 12.25 and s_vw == 403 and s_ow == 200 and owj["report_id"].startswith("COL-"),
11283
+ "cli": cli_v.returncode == 0 and "15/9-F-5" in cli_v.stdout and "BELOW 1.0" in cli_v.stdout and cli_f.returncode == 0 and "declination -13.69" in cli_f.stdout
11284
+ and cli_w.returncode == 0 and "collision" in cli_w.stdout,
11285
+ "report": rd["ok"] and any("separation factor" in t_ for t_ in rd["text"]) and any("Well A" in t_ for t_ in rd["text"])
11286
+ and "collision.settings" in audit and "report.collision" in audit,
11287
+ "page": all(x in page_src for x in ("VIEWS.collision", "['#/collision', 'Collision']", "/api/collision/settings", "/api/collision/write", "coChart")),
11288
+ "help": not H.check("collision") and H.VIEW_TOPIC.get("collision") == "collision" and "WMM2025" in (pkg / "help" / "collision.md").read_text(encoding="utf-8"),
11289
+ }
11290
+ failed4 = [k for k, v_ in c4.items() if not v_]
11291
+ ok(not failed4,
11292
+ "CR4 a workspace's wells: those with a position, an elevation and a survey checked, the one without listed with why, the field "
11293
+ "at each well; on the API anyone reads it and only an operator keeps a rule (with its source; a k below 0.5 refused) or a hole size "
11294
+ "(inches, 2 to 40) or writes the report; the CLI (the worked example, the field at a place and date, the report); the report as "
11295
+ "PDF; the tab and the help"
11296
+ + (f" [failed: {', '.join(failed4)}]" if failed4 else ""))
11297
+
11298
+
10916
11299
  def main() -> int:
10917
11300
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
10918
11301
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -11010,6 +11393,8 @@ def main() -> int:
11010
11393
  section_cn_four(tmp)
11011
11394
  section_co_invoice_check(tmp)
11012
11395
  section_cp_rig_time(tmp)
11396
+ section_cq_tied_together(tmp)
11397
+ section_cr_collision(tmp)
11013
11398
  if _FAILS:
11014
11399
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
11015
11400
  for f in _FAILS: