gea-program 0.38.0__tar.gz → 0.40.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 (315) hide show
  1. {gea_program-0.38.0/gea_program.egg-info → gea_program-0.40.0}/PKG-INFO +25 -1
  2. {gea_program-0.38.0 → gea_program-0.40.0}/README.md +24 -0
  3. {gea_program-0.38.0 → gea_program-0.40.0}/gea/__init__.py +1 -1
  4. {gea_program-0.38.0 → gea_program-0.40.0}/gea/__main__.py +70 -2
  5. {gea_program-0.38.0 → gea_program-0.40.0}/gea/acceptance_tests.py +312 -0
  6. gea_program-0.40.0/gea/drill_string.py +631 -0
  7. gea_program-0.40.0/gea/help/drillstring.md +11 -0
  8. gea_program-0.40.0/gea/help/simulation.md +11 -0
  9. {gea_program-0.38.0 → gea_program-0.40.0}/gea/helplib.py +3 -1
  10. gea_program-0.40.0/gea/reference/mud_motor_specs.json +1000 -0
  11. gea_program-0.40.0/gea/reference/volve_f4_interpretation.las.gz +0 -0
  12. gea_program-0.40.0/gea/reference/volve_f4_logs.provenance.json +46 -0
  13. gea_program-0.40.0/gea/reference/volve_f4_lwd.las.gz +0 -0
  14. {gea_program-0.38.0 → gea_program-0.40.0}/gea/service.py +31 -0
  15. gea_program-0.40.0/gea/simulation.py +424 -0
  16. {gea_program-0.38.0 → gea_program-0.40.0}/gea/tool_catalog.py +6 -1
  17. {gea_program-0.38.0 → gea_program-0.40.0}/gea/web/app.html +348 -12
  18. {gea_program-0.38.0 → gea_program-0.40.0/gea_program.egg-info}/PKG-INFO +25 -1
  19. {gea_program-0.38.0 → gea_program-0.40.0}/gea_program.egg-info/SOURCES.txt +8 -0
  20. {gea_program-0.38.0 → gea_program-0.40.0}/pyproject.toml +1 -1
  21. {gea_program-0.38.0 → gea_program-0.40.0}/LICENSE +0 -0
  22. {gea_program-0.38.0 → gea_program-0.40.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  23. {gea_program-0.38.0 → gea_program-0.40.0}/gea/accuracy_statement.py +0 -0
  24. {gea_program-0.38.0 → gea_program-0.40.0}/gea/alarm_engine.py +0 -0
  25. {gea_program-0.38.0 → gea_program-0.40.0}/gea/backup.py +0 -0
  26. {gea_program-0.38.0 → gea_program-0.40.0}/gea/bench.py +0 -0
  27. {gea_program-0.38.0 → gea_program-0.40.0}/gea/blind_harness.py +0 -0
  28. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  29. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  30. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  31. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  32. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  33. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  34. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  35. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  36. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  37. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  38. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  39. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  40. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  41. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  42. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  43. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  44. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  45. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  46. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  47. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  48. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  49. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  50. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  51. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  52. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  53. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  54. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  55. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  56. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  57. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  58. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  59. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  60. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  61. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  62. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  63. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  64. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  65. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  66. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  67. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  68. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  69. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  70. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  71. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  72. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  73. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  74. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  75. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  76. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  77. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  78. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  79. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  80. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  81. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  82. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  83. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  84. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  85. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  86. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  87. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  88. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  89. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  90. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  91. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  92. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  93. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  94. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  95. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/med_160_sapropels.txt +0 -0
  96. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  97. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  98. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  99. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  100. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  101. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  102. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  103. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  104. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  105. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  106. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  107. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  108. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  109. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  110. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  111. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  112. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  113. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  114. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  115. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  116. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  117. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  118. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  119. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  120. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  121. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  122. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  123. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  124. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  125. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  126. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  127. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  128. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  129. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  130. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  131. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  132. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog_map.py +0 -0
  133. {gea_program-0.38.0 → gea_program-0.40.0}/gea/catalog_register.py +0 -0
  134. {gea_program-0.38.0 → gea_program-0.40.0}/gea/certificates.py +0 -0
  135. {gea_program-0.38.0 → gea_program-0.40.0}/gea/cli.py +0 -0
  136. {gea_program-0.38.0 → gea_program-0.40.0}/gea/client_reports.py +0 -0
  137. {gea_program-0.38.0 → gea_program-0.40.0}/gea/config_versioning.py +0 -0
  138. {gea_program-0.38.0 → gea_program-0.40.0}/gea/conformance.py +0 -0
  139. {gea_program-0.38.0 → gea_program-0.40.0}/gea/control_room.py +0 -0
  140. {gea_program-0.38.0 → gea_program-0.40.0}/gea/correlation.py +0 -0
  141. {gea_program-0.38.0 → gea_program-0.40.0}/gea/dashboard.py +0 -0
  142. {gea_program-0.38.0 → gea_program-0.40.0}/gea/deviation.py +0 -0
  143. {gea_program-0.38.0 → gea_program-0.40.0}/gea/dlis.py +0 -0
  144. {gea_program-0.38.0 → gea_program-0.40.0}/gea/dnp3.py +0 -0
  145. {gea_program-0.38.0 → gea_program-0.40.0}/gea/doctor.py +0 -0
  146. {gea_program-0.38.0 → gea_program-0.40.0}/gea/downhole_engine.py +0 -0
  147. {gea_program-0.38.0 → gea_program-0.40.0}/gea/drift_monitor.py +0 -0
  148. {gea_program-0.38.0 → gea_program-0.40.0}/gea/earth_model.py +0 -0
  149. {gea_program-0.38.0 → gea_program-0.40.0}/gea/economics.py +0 -0
  150. {gea_program-0.38.0 → gea_program-0.40.0}/gea/edr.py +0 -0
  151. {gea_program-0.38.0 → gea_program-0.40.0}/gea/elevation.py +0 -0
  152. {gea_program-0.38.0 → gea_program-0.40.0}/gea/etp.py +0 -0
  153. {gea_program-0.38.0 → gea_program-0.40.0}/gea/etp12_schemas.json +0 -0
  154. {gea_program-0.38.0 → gea_program-0.40.0}/gea/example_register_map.json +0 -0
  155. {gea_program-0.38.0 → gea_program-0.40.0}/gea/fat_sat.py +0 -0
  156. {gea_program-0.38.0 → gea_program-0.40.0}/gea/files.py +0 -0
  157. {gea_program-0.38.0 → gea_program-0.40.0}/gea/fixtures/README.md +0 -0
  158. {gea_program-0.38.0 → gea_program-0.40.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  159. {gea_program-0.38.0 → gea_program-0.40.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  160. {gea_program-0.38.0 → gea_program-0.40.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  161. {gea_program-0.38.0 → gea_program-0.40.0}/gea/follower.py +0 -0
  162. {gea_program-0.38.0 → gea_program-0.40.0}/gea/forward_model.py +0 -0
  163. {gea_program-0.38.0 → gea_program-0.40.0}/gea/friction_priors.py +0 -0
  164. {gea_program-0.38.0 → gea_program-0.40.0}/gea/gamma.py +0 -0
  165. {gea_program-0.38.0 → gea_program-0.40.0}/gea/gauge_aging.py +0 -0
  166. {gea_program-0.38.0 → gea_program-0.40.0}/gea/gauge_specs.py +0 -0
  167. {gea_program-0.38.0 → gea_program-0.40.0}/gea/geodesy.py +0 -0
  168. {gea_program-0.38.0 → gea_program-0.40.0}/gea/gravity_reference.py +0 -0
  169. {gea_program-0.38.0 → gea_program-0.40.0}/gea/hartip.py +0 -0
  170. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/TESTER_GUIDE.md +0 -0
  171. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/alarms.md +0 -0
  172. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/audit-update.md +0 -0
  173. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/backup.md +0 -0
  174. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/catalog.md +0 -0
  175. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/charts.md +0 -0
  176. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/conformance.md +0 -0
  177. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/control-room.md +0 -0
  178. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/data-in.md +0 -0
  179. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/dlis.md +0 -0
  180. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/doctor.md +0 -0
  181. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/drift.md +0 -0
  182. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/economics.md +0 -0
  183. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/edr.md +0 -0
  184. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/etp.md +0 -0
  185. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/field-ports.md +0 -0
  186. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/files.md +0 -0
  187. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/historian.md +0 -0
  188. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/instruments.md +0 -0
  189. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/interval.md +0 -0
  190. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/map.md +0 -0
  191. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/month-end.md +0 -0
  192. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/notifications.md +0 -0
  193. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/osdu.md +0 -0
  194. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/patches.md +0 -0
  195. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/ppdm.md +0 -0
  196. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/quakeml.md +0 -0
  197. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/quality.md +0 -0
  198. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/seedlink.md +0 -0
  199. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/seismic.md +0 -0
  200. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/setup.md +0 -0
  201. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/site.md +0 -0
  202. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/sites.md +0 -0
  203. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/slip.md +0 -0
  204. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/soak.md +0 -0
  205. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/sra.md +0 -0
  206. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/start.md +0 -0
  207. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/structure.md +0 -0
  208. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/tools.md +0 -0
  209. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/transients.md +0 -0
  210. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/upkeep.md +0 -0
  211. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/vibration.md +0 -0
  212. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/well-report.md +0 -0
  213. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/well-tests.md +0 -0
  214. {gea_program-0.38.0 → gea_program-0.40.0}/gea/help/well3d.md +0 -0
  215. {gea_program-0.38.0 → gea_program-0.40.0}/gea/housekeeping.py +0 -0
  216. {gea_program-0.38.0 → gea_program-0.40.0}/gea/interval.py +0 -0
  217. {gea_program-0.38.0 → gea_program-0.40.0}/gea/inverse_engine.py +0 -0
  218. {gea_program-0.38.0 → gea_program-0.40.0}/gea/jobs.py +0 -0
  219. {gea_program-0.38.0 → gea_program-0.40.0}/gea/live_ports.py +0 -0
  220. {gea_program-0.38.0 → gea_program-0.40.0}/gea/loadtest.py +0 -0
  221. {gea_program-0.38.0 → gea_program-0.40.0}/gea/matplotlib_demo.py +0 -0
  222. {gea_program-0.38.0 → gea_program-0.40.0}/gea/modbus.py +0 -0
  223. {gea_program-0.38.0 → gea_program-0.40.0}/gea/modbus_rtu.py +0 -0
  224. {gea_program-0.38.0 → gea_program-0.40.0}/gea/model_card.py +0 -0
  225. {gea_program-0.38.0 → gea_program-0.40.0}/gea/month_end.py +0 -0
  226. {gea_program-0.38.0 → gea_program-0.40.0}/gea/mqtt_port.py +0 -0
  227. {gea_program-0.38.0 → gea_program-0.40.0}/gea/notify.py +0 -0
  228. {gea_program-0.38.0 → gea_program-0.40.0}/gea/opcua_port.py +0 -0
  229. {gea_program-0.38.0 → gea_program-0.40.0}/gea/operator_app.py +0 -0
  230. {gea_program-0.38.0 → gea_program-0.40.0}/gea/osdu.py +0 -0
  231. {gea_program-0.38.0 → gea_program-0.40.0}/gea/patches.py +0 -0
  232. {gea_program-0.38.0 → gea_program-0.40.0}/gea/permits.py +0 -0
  233. {gea_program-0.38.0 → gea_program-0.40.0}/gea/piwebapi.py +0 -0
  234. {gea_program-0.38.0 → gea_program-0.40.0}/gea/ports.py +0 -0
  235. {gea_program-0.38.0 → gea_program-0.40.0}/gea/ppdm.py +0 -0
  236. {gea_program-0.38.0 → gea_program-0.40.0}/gea/profile_catalog.py +0 -0
  237. {gea_program-0.38.0 → gea_program-0.40.0}/gea/project.py +0 -0
  238. {gea_program-0.38.0 → gea_program-0.40.0}/gea/qt6_downhole_app.py +0 -0
  239. {gea_program-0.38.0 → gea_program-0.40.0}/gea/quakeml.py +0 -0
  240. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reconciler.py +0 -0
  241. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  242. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  243. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  244. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/eia_prices.provenance.json +0 -0
  245. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  246. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  247. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  248. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  249. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  250. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/tool_sources.json +0 -0
  251. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/us_outline.json +0 -0
  252. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/us_outline.provenance.json +0 -0
  253. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_3d.provenance.json +0 -0
  254. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_surfaces.json.gz +0 -0
  255. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  256. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  257. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  258. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  259. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  260. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  261. {gea_program-0.38.0 → gea_program-0.40.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  262. {gea_program-0.38.0 → gea_program-0.40.0}/gea/rock_inventory.py +0 -0
  263. {gea_program-0.38.0 → gea_program-0.40.0}/gea/sample_record.py +0 -0
  264. {gea_program-0.38.0 → gea_program-0.40.0}/gea/sample_well_profile.csv +0 -0
  265. {gea_program-0.38.0 → gea_program-0.40.0}/gea/sbom.py +0 -0
  266. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seedlink.py +0 -0
  267. {gea_program-0.38.0 → gea_program-0.40.0}/gea/segy.py +0 -0
  268. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic.py +0 -0
  269. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_array.py +0 -0
  270. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_assoc.py +0 -0
  271. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_detect.py +0 -0
  272. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_film.py +0 -0
  273. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_harmonic.py +0 -0
  274. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_qc.py +0 -0
  275. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_response.py +0 -0
  276. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_signature.py +0 -0
  277. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_track.py +0 -0
  278. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_uncertainty.py +0 -0
  279. {gea_program-0.38.0 → gea_program-0.40.0}/gea/seismic_unlisted.py +0 -0
  280. {gea_program-0.38.0 → gea_program-0.40.0}/gea/sensor_swap.py +0 -0
  281. {gea_program-0.38.0 → gea_program-0.40.0}/gea/series_cache.py +0 -0
  282. {gea_program-0.38.0 → gea_program-0.40.0}/gea/service_life.py +0 -0
  283. {gea_program-0.38.0 → gea_program-0.40.0}/gea/setup_checklist.py +0 -0
  284. {gea_program-0.38.0 → gea_program-0.40.0}/gea/shapefile.py +0 -0
  285. {gea_program-0.38.0 → gea_program-0.40.0}/gea/shell.py +0 -0
  286. {gea_program-0.38.0 → gea_program-0.40.0}/gea/shut_in.py +0 -0
  287. {gea_program-0.38.0 → gea_program-0.40.0}/gea/site_map.py +0 -0
  288. {gea_program-0.38.0 → gea_program-0.40.0}/gea/sla_report.py +0 -0
  289. {gea_program-0.38.0 → gea_program-0.40.0}/gea/slip.py +0 -0
  290. {gea_program-0.38.0 → gea_program-0.40.0}/gea/soak.py +0 -0
  291. {gea_program-0.38.0 → gea_program-0.40.0}/gea/sra.py +0 -0
  292. {gea_program-0.38.0 → gea_program-0.40.0}/gea/store_forward.py +0 -0
  293. {gea_program-0.38.0 → gea_program-0.40.0}/gea/strata_join.py +0 -0
  294. {gea_program-0.38.0 → gea_program-0.40.0}/gea/structure.py +0 -0
  295. {gea_program-0.38.0 → gea_program-0.40.0}/gea/supervisor.py +0 -0
  296. {gea_program-0.38.0 → gea_program-0.40.0}/gea/survey_cmd.py +0 -0
  297. {gea_program-0.38.0 → gea_program-0.40.0}/gea/survey_view.py +0 -0
  298. {gea_program-0.38.0 → gea_program-0.40.0}/gea/telemetry.py +0 -0
  299. {gea_program-0.38.0 → gea_program-0.40.0}/gea/tool_library.py +0 -0
  300. {gea_program-0.38.0 → gea_program-0.40.0}/gea/tool_taxonomy.py +0 -0
  301. {gea_program-0.38.0 → gea_program-0.40.0}/gea/transient.py +0 -0
  302. {gea_program-0.38.0 → gea_program-0.40.0}/gea/vibration.py +0 -0
  303. {gea_program-0.38.0 → gea_program-0.40.0}/gea/well3d.py +0 -0
  304. {gea_program-0.38.0 → gea_program-0.40.0}/gea/well_assembler.py +0 -0
  305. {gea_program-0.38.0 → gea_program-0.40.0}/gea/well_report.py +0 -0
  306. {gea_program-0.38.0 → gea_program-0.40.0}/gea/well_test_validation.py +0 -0
  307. {gea_program-0.38.0 → gea_program-0.40.0}/gea/wits0.py +0 -0
  308. {gea_program-0.38.0 → gea_program-0.40.0}/gea/witsml.py +0 -0
  309. {gea_program-0.38.0 → gea_program-0.40.0}/gea/workspace.py +0 -0
  310. {gea_program-0.38.0 → gea_program-0.40.0}/gea_program.egg-info/dependency_links.txt +0 -0
  311. {gea_program-0.38.0 → gea_program-0.40.0}/gea_program.egg-info/entry_points.txt +0 -0
  312. {gea_program-0.38.0 → gea_program-0.40.0}/gea_program.egg-info/requires.txt +0 -0
  313. {gea_program-0.38.0 → gea_program-0.40.0}/gea_program.egg-info/top_level.txt +0 -0
  314. {gea_program-0.38.0 → gea_program-0.40.0}/setup.cfg +0 -0
  315. {gea_program-0.38.0 → gea_program-0.40.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.38.0
3
+ Version: 0.40.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,30 @@ 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
+ ### Drill string
652
+
653
+ The Drill String tab holds each run's string from the bit up - drawn as a
654
+ build diagram - and follows every component's hours, footage and, for a
655
+ motor, revolutions from the drilling record across runs and wells against
656
+ its declared service interval. The downhole motor tracker charts the
657
+ differential against the maker's limits, the stalls, the motor's and the
658
+ bit's speed and a torque estimate, and judges the motor's stability from
659
+ the slow oscillations of the torque and the differential, block by block.
660
+ The tool catalogue carries 41 downhole motors with the maker's figures.
661
+ `gea drillstring`, `gea tools --action motors`, `gea help drillstring`.
662
+
663
+ ### Simulation
664
+
665
+ The Simulation tab drills a well again on the screen: the bit runs down its
666
+ path in 3D among the mapped formations, on the clock it was drilled by,
667
+ while a mud-log panel beside it grows with the hole - the formations, the
668
+ rock and fluid volumes, the logs and the drilling parameters - and a
669
+ readout says what the record carries at the bit. Volve 15/9-F-4 ships with
670
+ its logging-while-drilling record and Schlumberger's interpretation of it
671
+ (mineral volumes, porosity, water saturation, hydrocarbon volume); every
672
+ well with a drilling record plays on its recorder's own clock.
673
+ `gea simulation`, `gea help simulation`.
674
+
651
675
  ### The client well report
652
676
 
653
677
  `gea well-report` writes one printable document per well and one for the
@@ -609,6 +609,30 @@ 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
+ ### Drill string
613
+
614
+ The Drill String tab holds each run's string from the bit up - drawn as a
615
+ build diagram - and follows every component's hours, footage and, for a
616
+ motor, revolutions from the drilling record across runs and wells against
617
+ its declared service interval. The downhole motor tracker charts the
618
+ differential against the maker's limits, the stalls, the motor's and the
619
+ bit's speed and a torque estimate, and judges the motor's stability from
620
+ the slow oscillations of the torque and the differential, block by block.
621
+ The tool catalogue carries 41 downhole motors with the maker's figures.
622
+ `gea drillstring`, `gea tools --action motors`, `gea help drillstring`.
623
+
624
+ ### Simulation
625
+
626
+ The Simulation tab drills a well again on the screen: the bit runs down its
627
+ path in 3D among the mapped formations, on the clock it was drilled by,
628
+ while a mud-log panel beside it grows with the hole - the formations, the
629
+ rock and fluid volumes, the logs and the drilling parameters - and a
630
+ readout says what the record carries at the bit. Volve 15/9-F-4 ships with
631
+ its logging-while-drilling record and Schlumberger's interpretation of it
632
+ (mineral volumes, porosity, water saturation, hydrocarbon volume); every
633
+ well with a drilling record plays on its recorder's own clock.
634
+ `gea simulation`, `gea help simulation`.
635
+
612
636
  ### The client well report
613
637
 
614
638
  `gea well-report` writes one printable document per well and one for the
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.38.0"
126
+ __version__ = "0.40.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -285,8 +285,8 @@ def main(argv=None) -> int:
285
285
  p_tl.add_argument("--station", type=str, action="append", default=None, help="a station as DEPTH_FT:TOOL, e.g. 9850:quartz_pt_geoq177_16k")
286
286
  p_tl.add_argument("--surface-temp-f", type=float, default=75.0); p_tl.add_argument("--temp-gradient-f-ft", type=float, default=0.018)
287
287
  p_tl.add_argument("--surface-pressure-psi", type=float, default=14.7); p_tl.add_argument("--pressure-gradient-psi-ft", type=float, default=0.465)
288
- p_tl.add_argument("--action", choices=["classes", "tutorial", "fleet", "fleet-add", "fleet-remove"], default=None,
289
- help="the downhole tool classes in use across the industry: classes (--category, --find), tutorial --class ID, and the workspace's own fleet (fleet, fleet-add, fleet-remove)")
288
+ p_tl.add_argument("--action", choices=["classes", "tutorial", "fleet", "fleet-add", "fleet-remove", "motors"], default=None,
289
+ help="the downhole tool classes in use across the industry: classes (--category, --find), tutorial --class ID, the workspace's own fleet (fleet, fleet-add, fleet-remove), and the downhole motor specifications (motors)")
290
290
  p_tl.add_argument("--category", type=str, default=None, help="classes: one category (bits, drive, measure, bha, casing, completion, logging, fishing, testing)")
291
291
  p_tl.add_argument("--find", type=str, default="", help="classes: words to look for in the name and description")
292
292
  p_tl.add_argument("--class", dest="class_id", type=str, default=None, help="tutorial / fleet-add: the tool class id, e.g. pdm_motor")
@@ -516,6 +516,20 @@ def main(argv=None) -> int:
516
516
  p_3d.add_argument("--workspace", type=str, default=None)
517
517
  p_3d.add_argument("--json", action="store_true")
518
518
 
519
+ p_ds = sub.add_parser("drillstring", help="the drill string: each declared run's components from the bit up, their usage from the drilling record and their service life; the downhole motor's statistics and the stability of its oscillations")
520
+ p_ds.add_argument("--workspace", type=str, required=True)
521
+ p_ds.add_argument("--well", type=str, default=None)
522
+ p_ds.add_argument("--declare", type=str, default=None, help="a JSON file declaring a run: {well_id, name, start, end, components:[{class_id, maker, model, serial, od_in, length_ft, motor_id | motor{...}, service_limit, service_basis, service_source}]}")
523
+ p_ds.add_argument("--remove", type=str, default=None, help="a run id to remove")
524
+ p_ds.add_argument("--actor", type=str, default="cli")
525
+ p_ds.add_argument("--json", action="store_true")
526
+
527
+ p_sm = sub.add_parser("simulation", help="the simulation in words: the scenes (Volve 15/9-F-4 with its logs and interpretation, Volve F-5 and F-10 on the survey clock, every drilling-recorder well), and what the record says at a depth (the Simulation tab replays it)")
528
+ p_sm.add_argument("--workspace", type=str, default=None)
529
+ p_sm.add_argument("--scene", type=str, default=None, help="e.g. volve-f4, or well:Well-30636033")
530
+ p_sm.add_argument("--at", type=float, default=None, help="a measured depth, in the scene's unit (m for Volve, ft for a workspace well)")
531
+ p_sm.add_argument("--json", action="store_true")
532
+
519
533
  p_wl = sub.add_parser("wall", help="the control-room board as text: a line per well, worst first, its state and why (the same board the wall screen shows)")
520
534
  p_wl.add_argument("--workspace", type=str, default=None, help="the workspace folder, e.g. C:\\site")
521
535
  p_wl.add_argument("--json", action="store_true")
@@ -2035,6 +2049,56 @@ def main(argv=None) -> int:
2035
2049
  r = _WR.write_pad(ws, __version__, actor=a.actor)
2036
2050
  print(f"pad: {r['paths']['html']}")
2037
2051
  return 0
2052
+ elif a.cmd == "drillstring":
2053
+ import json as _json
2054
+ from . import drill_string as _DS
2055
+ from .cli import _safe_stdout
2056
+ from .workspace import Workspace, WorkspaceError
2057
+ _safe_stdout()
2058
+ try:
2059
+ ws = Workspace(a.workspace)
2060
+ except WorkspaceError as e:
2061
+ raise SystemExit(f"drillstring: {e}")
2062
+ try:
2063
+ if a.declare:
2064
+ with open(a.declare, encoding="utf-8") as f:
2065
+ d = _json.load(f)
2066
+ r = _DS.save_run(ws, a.actor, d.get("well_id", ""), d.get("name", ""), d.get("components") or [], d.get("start"), d.get("end"),
2067
+ d.get("depth_in_ft"), d.get("depth_out_ft"), d.get("note", ""), d.get("run_id"))
2068
+ print(f"declared {r['id']}: {r['name']} on {r['well_id']}, {len(r['components'])} component(s)")
2069
+ return 0
2070
+ if a.remove:
2071
+ _DS.remove_run(ws, a.actor, a.remove); print(f"removed {a.remove}"); return 0
2072
+ p = _DS.page(ws, a.well)
2073
+ except (ValueError, KeyError, OSError, WorkspaceError) as e:
2074
+ raise SystemExit(f"drillstring: {e.args[0] if e.args else e}")
2075
+ print(_json.dumps(p, default=str) if a.json else _DS.text(p))
2076
+ return 0
2077
+ elif a.cmd == "simulation":
2078
+ import json as _json
2079
+ from . import simulation as _SIM
2080
+ from .cli import _safe_stdout
2081
+ from .workspace import Workspace, WorkspaceError
2082
+ _safe_stdout()
2083
+ ws = None
2084
+ if a.workspace:
2085
+ try:
2086
+ ws = Workspace(a.workspace)
2087
+ except WorkspaceError as e:
2088
+ raise SystemExit(f"simulation: {e}")
2089
+ if not a.scene:
2090
+ for s_ in _SIM.scenes(ws):
2091
+ print(f"{s_['id']:<28} {s_['title']} ({s_['note']})")
2092
+ return 0
2093
+ try:
2094
+ sim = _SIM.scene(ws, a.scene)
2095
+ except KeyError as e:
2096
+ raise SystemExit(f"simulation: {e.args[0] if e.args else e}")
2097
+ if a.json:
2098
+ print(_json.dumps(sim if a.at is None else _SIM.at(sim, a.at * (0.3048 if sim.get('depth_unit') == 'ft' else 1.0)), default=str))
2099
+ else:
2100
+ print(_SIM.text(sim, a.at))
2101
+ return 0
2038
2102
  elif a.cmd in ("trajectory", "well3d"):
2039
2103
  import json as _json
2040
2104
  from . import well3d as _W3
@@ -2610,6 +2674,10 @@ def main(argv=None) -> int:
2610
2674
  from . import tool_taxonomy as _TX
2611
2675
  from .cli import _safe_stdout
2612
2676
  _safe_stdout() # the cited figures carry fractions and degree signs
2677
+ if a.action == "motors":
2678
+ from . import drill_string as _DS
2679
+ print(_json.dumps(_DS.catalogue(), indent=1) if a.json else _DS.motor_text())
2680
+ return 0
2613
2681
  if a.action == "classes":
2614
2682
  cs = _TX.classes(a.category, a.find)
2615
2683
  if a.category and a.category not in _TX.CATEGORY_NAMES:
@@ -9137,6 +9137,316 @@ def section_cg_well_report(tmp: str) -> None:
9137
9137
  + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
9138
9138
 
9139
9139
 
9140
+ def section_ch_simulation(tmp: str) -> None:
9141
+ """Section CH - the simulation: Volve 15/9-F-4's logs and their interpretation shipped verbatim and checked against
9142
+ themselves, read back at a depth to the value in the file; the bit on the clock each record carries (stepped along
9143
+ measured depth where none was recorded, on the survey stations' times, on a drilling recorder's own clock) with the
9144
+ hole never behind the bit; a workspace well's path from its survey or from the recorder's MD-TVD pairs; the API, the
9145
+ CLI, the tab and its help page."""
9146
+ import gzip as _gz
9147
+ import hashlib as _hl
9148
+ import http.cookiejar
9149
+ import subprocess as _sp
9150
+ import tempfile as _tf
9151
+ import urllib.request
9152
+ import urllib.error
9153
+ from . import edr as E
9154
+ from . import helplib as H
9155
+ from . import ports as PT
9156
+ from . import simulation as SIM
9157
+ from . import well3d as W
9158
+ from .workspace import Workspace
9159
+ from .service import Service, Users
9160
+ pkg = Path(__file__).parent
9161
+ d = Path(tmp, "ch"); d.mkdir()
9162
+ prov = json.loads((pkg / "reference" / "volve_f4_logs.provenance.json").read_text(encoding="utf-8"))
9163
+ hashes = all(_hl.sha256((pkg / "reference" / f["file"]).read_bytes()).hexdigest() == f["sha256"]
9164
+ and _hl.sha256(_gz.decompress((pkg / "reference" / f["file"]).read_bytes())).hexdigest() == f["sha256_uncompressed"]
9165
+ and len(f["source"]["commit"]) == 40 for f in prov["files"].values())
9166
+ files = {}
9167
+ for k, f in prov["files"].items():
9168
+ p = d / (k + ".las"); p.write_bytes(_gz.decompress((pkg / "reference" / f["file"]).read_bytes())); files[k] = PT.read_las(str(p))
9169
+ itp, lwd = files["interpretation"], files["lwd"]
9170
+
9171
+ def ch(st, n):
9172
+ v = np.asarray(st.channels[n].values, dtype=float).copy(); v[v <= -999] = np.nan; return v
9173
+ mins = sum(np.nan_to_num(ch(itp, n)) for n in ("VQFM", "VCLA", "VCLC", "VDOL", "VPYR", "VSID", "VANH", "VEVA", "VCOA"))
9174
+ phit, swt, vuhy = ch(itp, "PHIT"), ch(itp, "SWT"), ch(itp, "VUHY")
9175
+ sim = SIM.scene(None, "volve-f4")
9176
+ col = sim["columns"][0]
9177
+ sums = [sum(x or 0 for x in r) for r in col["f"]]
9178
+ a = SIM.at(sim, 3300.0)
9179
+ md_l, gr = np.asarray(lwd.index, dtype=float), ch(lwd, "GRMA")
9180
+ gr_hand = float(np.interp(3300.0, md_l, gr))
9181
+ md_i = np.asarray(itp.index, dtype=float)
9182
+ rp = sim["scene"]["wells"][0]["replay"]
9183
+ c1 = {
9184
+ "shipped": hashes and "may not be sold" in prov["license"] and "Equinor" in prov["attribution"] and len(lwd.index) == 3636 and len(itp.index) == 3531
9185
+ and abs(float(lwd.index[0]) - 2955.95) < 1e-6,
9186
+ "self_consistent": float(np.nanmax(np.abs(mins + phit - 1))) < 2e-3 and float(np.nanmax(np.abs(vuhy - phit * (1 - swt)))) < 1e-3,
9187
+ "volumes": col["parts"][:2] == ["Water (in the pores)", "Hydrocarbon (in the pores)"] and min(sums) > 0.995 and max(sums) < 1.005
9188
+ and abs(a["columns"]["Rock and fluid volumes"]["Hydrocarbon (in the pores)"] - float(vuhy[int(np.argmin(np.abs(md_i - 3300.0)))])) < 2e-3,
9189
+ "read_back": abs(a["tracks"]["Gamma ray"]["value"] - gr_hand) < 0.01 * max(1.0, abs(gr_hand)) and a["formation"] == "Hugin Fm. VOLVE"
9190
+ and {"gr", "rhob", "nphi", "rt", "phit", "sw", "k"} <= {t["id"] for t in sim["tracks"]},
9191
+ "depth_clock": sim["clock"]["kind"] == "measured depth" and len(rp["md"]) == SIM.DEPTH_STEPS and rp["md"][0] == 0.0 and abs(rp["md"][-1] - 3510.0) < 0.01
9192
+ and all(t is None for t in rp["t"]) and "no drilling times" in sim["clock"]["note"],
9193
+ }
9194
+ failed1 = [k for k, v in c1.items() if not v]
9195
+ ok(not failed1,
9196
+ "CH1 Volve 15/9-F-4's logging-while-drilling record and Schlumberger's interpretation of it shipped verbatim with their hashes, the "
9197
+ "licence and the attribution; the interpretation's mineral volumes and total porosity summing to one and its hydrocarbon volume "
9198
+ "equal to the porosity times one less the water saturation; the volume column summing to one; the gamma ray read back at 3,300 m as "
9199
+ "the file has it, in the Hugin formation; the bit stepped along measured depth because the survey carries no drilling times"
9200
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
9201
+
9202
+ f5 = SIM.scene(None, "volve-f5"); f5w = next(w for w in W.volve_scene()["wells"] if w["id"] == "F-5")
9203
+ r5 = f5["scene"]["wells"][0]["replay"]
9204
+ ev = E.selftest_events(str(d))
9205
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
9206
+ wb = ws.add_well_file(ev["path"], display="Well B", actor="tester"); wc = ws.add_well_file(ev["path"], display="Well C", actor="tester")
9207
+ ws.set_well(wb["id"], surface={"lat": 31.42, "lon": -104.10, "datum": "NAD83", "elevation": {"ft": 3000.0, "datum": "KB", "source": "test plat"}}, actor="tester")
9208
+ csv_ok = "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"
9209
+ W.attach_trajectory(ws, wb["id"], "s.csv", csv_ok.encode(), "tester", "true", "ft", None, None, None, "test survey")
9210
+ ws.refresh_dashboard(actor="tester")
9211
+ rec = json.loads(Path(ws.reports_dir, "wells", wc["id"], "drilling_record.json").read_text(encoding="utf-8"))
9212
+ not_built = None
9213
+ import shutil as _sh
9214
+ _sh.rmtree(Path(wsp, "cache", "series", wc["id"]), ignore_errors=True) # no chart cache yet: the read-only path says so
9215
+ try:
9216
+ SIM.scene(ws, "well:" + wc["id"], build_cache=False)
9217
+ except KeyError as e:
9218
+ not_built = str(e)
9219
+ sb, sc_ = SIM.scene(ws, "well:" + wb["id"]), SIM.scene(ws, "well:" + wc["id"])
9220
+ rc = sc_["scene"]["wells"][0]["replay"]
9221
+ holes = [h for h in rc["hole"] if h is not None]
9222
+ bad = None
9223
+ try:
9224
+ SIM.scene(ws, "nope")
9225
+ except KeyError as e:
9226
+ bad = str(e)
9227
+ pc = sc_["scene"]["wells"][0]["path"]
9228
+ mt = rec["md_tvd"]
9229
+ c2 = {
9230
+ "survey_clock": f5["clock"]["kind"] == "recorded" and r5["t"] == f5w["replay"]["t"] and r5["md"] == f5w["replay"]["md"]
9231
+ and all(b >= a_ for a_, b in zip(r5["hole"], r5["hole"][1:])) and all(h >= m - 1e-6 for h, m in zip(r5["hole"], r5["md"])),
9232
+ "recorder_clock": sc_["clock"]["kind"] == "recorded" and rc["t"][0].startswith("2026-07-01") and not rc["t"][0].endswith("Z")
9233
+ and abs(max(holes) - rec["total_depth_md_ft"] * 0.3048) < 0.5 and all(b >= a_ - 1e-6 for a_, b in zip(holes, holes[1:]))
9234
+ and not_built is not None and "chart cache" in not_built,
9235
+ "tracks": {t["name"] for t in sc_["tracks"]} >= {"Gamma", "Rate of penetration"} and sc_["depth_unit"] == "ft"
9236
+ and any("density" in n for n in sc_["not_measured"]),
9237
+ "paths": sb["path_how"].startswith("the directional survey") and sb["scene"]["z_ref"] == "sea level"
9238
+ and sc_["path_how"].startswith("the drilling recorder's own MD-TVD pairs") and sc_["scene"]["z_ref"].startswith("the rig floor")
9239
+ and (mt.get("status") != "OK" or abs(pc["z"][pc["md"].index(next(m for m in pc["md"] if abs(m - mt["rows"][-1]["md_ft"] * 0.3048) < 0.01))] - mt["rows"][-1]["tvd_ft"] * 0.3048) < 0.01)
9240
+ and all(y == 0.0 for y in pc["y"]) and sc_["scene"]["no_north"] is True and sb["scene"]["no_north"] is False,
9241
+ "unknown": bad is not None and "nope" in bad,
9242
+ }
9243
+ failed2 = [k for k, v in c2.items() if not v]
9244
+ ok(not failed2,
9245
+ "CH2 the clock: 15/9-F-5's bit on its survey stations' own times as the 3D tab replays them; a drilling-recorder well's bit and "
9246
+ "bottom of the hole on the recorder's own clock (not called UTC), the hole never behind the bit and reaching the record's total "
9247
+ "depth; its channels by bit depth, and density, porosity and fluids named as not measured; its path from the attached survey, or "
9248
+ "from the recorder's MD-TVD pairs in one vertical plane below the rig floor when nothing places it; an unknown scene named"
9249
+ + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
9250
+
9251
+ Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer")
9252
+ svc = Service(wsp, port=0, scheduler=False).start()
9253
+ base_url = svc.url.rstrip("/")
9254
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
9255
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": "v", "password": "viewer-pass-1"}).encode())
9256
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
9257
+
9258
+ def getj(path):
9259
+ try:
9260
+ with o.open(base_url + path, timeout=120) as r_:
9261
+ return r_.status, json.loads(r_.read())
9262
+ except urllib.error.HTTPError as e:
9263
+ return e.code, json.loads(e.read())
9264
+ try:
9265
+ s_l, li = getj("/api/sim/scenes")
9266
+ s_f, jf = getj("/api/sim/scene/volve-f4")
9267
+ s_w, jw = getj("/api/sim/scene/well%3A" + wb["id"])
9268
+ s_n, _ = getj("/api/sim/scene/nope")
9269
+ finally:
9270
+ svc.stop()
9271
+ cli = _sp.run([sys.executable, "-m", "gea", "simulation"], capture_output=True, text=True, cwd=str(pkg.parent))
9272
+ cli_at = _sp.run([sys.executable, "-m", "gea", "simulation", "--scene", "volve-f4", "--at", "3300"], capture_output=True, text=True, cwd=str(pkg.parent))
9273
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
9274
+ c3 = {
9275
+ "api": s_l == 200 and [x["id"] for x in li["scenes"]][:3] == ["volve-f4", "volve-f5", "volve-f10"] and "well:" + wb["id"] in [x["id"] for x in li["scenes"]]
9276
+ and s_f == 200 and jf["columns"][0]["name"] == "Rock and fluid volumes" and s_w == 200 and jw["well_id"] == wb["id"] and s_n == 404,
9277
+ "cli": cli.returncode == 0 and "volve-f4" in cli.stdout and cli_at.returncode == 0 and "in Hugin Fm. VOLVE" in cli_at.stdout and "Hydrocarbon (in the pores)" in cli_at.stdout,
9278
+ "page": all(x in page_src for x in ("VIEWS.simulation", "['#/simulation', 'Simulation']", "simPanel", "simSeek", "show the log below the bit", "uCut",
9279
+ "/api/sim/scene/", "a gap in the record", "W3.simBit")),
9280
+ "help": any(t[0] == "simulation" for t in H.INDEX) and not H.check("simulation") and H.VIEW_TOPIC.get("simulation") == "simulation",
9281
+ }
9282
+ failed3 = [k for k, v in c3.items() if not v]
9283
+ ok(not failed3,
9284
+ "CH3 the API (the scenes, a Volve well and a workspace well by name, an unknown one 404), the CLI (the list, and what the record says "
9285
+ "at a depth), the Simulation tab - the 3D view's own renderer with the hole drilled so far and the string inside it, the mud-log panel "
9286
+ "growing with the hole, the readout, a click taking the bit to the depth - and its help page"
9287
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
9288
+
9289
+
9290
+ def section_ci_drill_string(tmp: str) -> None:
9291
+ """Section CI - the drill string: the downhole motor catalogue as the maker's sheets give it; a run declared from the
9292
+ bit up and refused when a part of it is unsourced or impossible; its usage, the motor's revolutions, speed, stalls and
9293
+ torque estimate equal to the hand sums over a labelled synthetic run; components followed across runs to their
9294
+ service interval; the stability rule finding a growing oscillation and a moved frequency and not calling red noise a
9295
+ peak; the API, the CLI, the tab and the help page."""
9296
+ import http.cookiejar
9297
+ import subprocess as _sp
9298
+ import urllib.request
9299
+ import urllib.error
9300
+ from datetime import datetime, timedelta
9301
+ from . import drill_string as DS
9302
+ from . import helplib as H
9303
+ from . import tool_catalog as TC
9304
+ from .workspace import Workspace
9305
+ from .service import Service, Users
9306
+ pkg = Path(__file__).parent
9307
+ d = Path(tmp, "ci"); d.mkdir()
9308
+ cat = DS.catalogue()
9309
+ ms = cat["motors"]
9310
+ m64, m89 = DS.motor_spec("msd-6-3-4-78-6.4"), DS.motor_spec("msd-8-3-8-89-4.0")
9311
+ c1 = {
9312
+ "catalogue": len(ms) == 41 and len({m["id"] for m in ms}) == 41 and cat["publisher"].startswith("MS Directional") and cat["url"].startswith("https://")
9313
+ and cat["fetched"] == "2026-10-17" and TC.catalog()["counts"]["motors"] == 41,
9314
+ "sane": all(m["flow_gpm"][0] < m["flow_gpm"][1] and 0 < m["rev_per_gal"] < 2 and m["max_operating_diff_psi"] <= m["max_diff_psi"]
9315
+ and m["stator_lobes"] == m["rotor_lobes"] + 1 and m["max_torque_ft_lb"] > 0 and m["od_in"] > 4 for m in ms),
9316
+ "as_printed": (m64["rev_per_gal"], m64["flow_gpm"], m64["max_operating_diff_psi"], m64["max_diff_psi"], m64["max_torque_ft_lb"]) == (0.28, [300, 600], 1000, 1820, 14360)
9317
+ and (m89["rev_per_gal"], m89["max_diff_psi"], m89["max_torque_ft_lb"], m89["max_operating_wob_lb"]) == (0.09, 1158, 32935, 70000)
9318
+ and "Motor Handbook" in m64["source"],
9319
+ }
9320
+ try:
9321
+ DS.motor_spec("nope"); c1["unknown"] = False
9322
+ except KeyError:
9323
+ c1["unknown"] = True
9324
+ failed1 = [k for k, v in c1.items() if not v]
9325
+ ok(not failed1,
9326
+ "CI1 the downhole motor catalogue: 41 power sections with the maker's flow range, revolutions per gallon, maximum operating and "
9327
+ "maximum differential, maximum torque and weight on bit as its spec sheets print them, the handbook and the date it was read; "
9328
+ "every lobe count one more on the stator than the rotor; counted in the tool catalogue; an unknown motor named"
9329
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
9330
+
9331
+ syn = DS.synthetic_motor_record(str(d))
9332
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
9333
+ w = ws.add_well_file(syn["path"], display="SYNTHETIC motor well", actor="tester")
9334
+ ws.refresh_dashboard(actor="tester")
9335
+ comps = [{"class_id": "pdc_bit", "maker": "Acme", "model": "X616", "serial": "B1", "od_in": 8.75, "length_ft": 1.2},
9336
+ {"class_id": "pdm_motor", "motor_id": "msd-6-3-4-78-6.4", "serial": "M100", "length_ft": 30, "service_limit": 20, "service_basis": "circulating_hours", "service_source": "rental contract"},
9337
+ {"class_id": "mwd_mud_pulse", "maker": "Acme", "model": "MWD", "serial": "W7", "od_in": 6.75, "length_ft": 32},
9338
+ {"class_id": "drilling_jar", "maker": "Acme", "model": "Jar", "serial": "J2", "od_in": 6.5, "length_ft": 31, "service_limit": 300, "service_basis": "rotating_hours", "service_source": "shop rule"}]
9339
+ refused = 0
9340
+ for bad in ([{"class_id": "packer"}], [{"class_id": "pdc_bit", "service_limit": 10}], [{"class_id": "drilling_jar", "service_limit": 10, "service_basis": "revolutions", "service_source": "s"}],
9341
+ [{"class_id": "pdm_motor", "motor": {"rev_per_gal": 0.3}}], [{"class_id": "pdm_motor", "motor": {"rev_per_gal": 0.3, "lobes": "seven", "source": "s"}}], []):
9342
+ try:
9343
+ DS.save_run(ws, "tester", w["id"], "bad", bad)
9344
+ except ValueError:
9345
+ refused += 1
9346
+ for args in (("nope", "r", comps), (w["id"], "r", comps, "2026-08-01T10:00", "2026-08-01T09:00")):
9347
+ try:
9348
+ DS.save_run(ws, "tester", *args)
9349
+ except (ValueError, KeyError):
9350
+ refused += 1
9351
+ r1 = DS.save_run(ws, "tester", w["id"], "Run 1", comps)
9352
+ half = (datetime.fromisoformat(syn["start"]) + timedelta(hours=8)).isoformat()
9353
+ r2 = DS.save_run(ws, "tester", w["id"], "Run 2 (second half)", [comps[1]], start=half)
9354
+ tr = DS.run_view(ws, r1)["track"]; u = tr["usage"]; st = tr["motor_stats"]; h = st["hourly"]
9355
+ gal = syn["gallons"]
9356
+ trk = {e["serial"]: e for e in DS.tracking(ws)}
9357
+ c2 = {
9358
+ "refused": refused == 8,
9359
+ "usage": u["circulating_hours"] == 16.0 and u["drilling_hours"] == 16.0 and abs(u["gallons"] - gal) < 1 and abs(u["footage_ft"] - 960.0) < 1.0
9360
+ and abs(u["revolutions"] - round(0.28 * gal)) < 2,
9361
+ "motor": h["motor_rpm"][0] == 140.0 and h["bit_rpm"][0] == 190.0 and abs(h["whirl_hz"][0] - round(140 / 60 * 7, 3)) < 1e-6
9362
+ and abs(st["torque_slope_ft_lb_per_psi"] - round(14360 / 1820, 3)) < 1e-9 and 4600 < h["torque_est_ft_lb"][0] < 4900,
9363
+ "stall": len(st["stalls"]) == 1 and st["stalls"][0]["at"] == syn["stall_at"] and st["stalls"][0]["seconds"] == 20 and st["stalls"][0]["peak_psi"] == 1900
9364
+ and 0.0 < st["hours_above_max_operating"] <= 0.01 and st["diff_on_bottom_psi"]["p50"] == 600.0,
9365
+ "tracking": trk["M100"]["totals"]["circulating_hours"] == 24.0 and len(trk["M100"]["runs"]) == 2 and trk["M100"]["life"]["status"] == "DUE"
9366
+ and trk["J2"]["life"]["status"] == "OK" and trk["J2"]["totals"]["revolutions"] == 0.0 and "life" not in trk["B1"],
9367
+ "audit": [a["action"] for a in ws.audit_log(200)].count("drillstring.run.declare") == 2,
9368
+ }
9369
+ failed2 = [k for k, v in c2.items() if not v]
9370
+ ok(not failed2,
9371
+ "CI2 a run from the bit up: an unknown class, a service interval with no source, revolutions for a jar, a motor's own figures with "
9372
+ "no spec sheet or a malformed lobe count, an empty string, an unknown well and a run that ends before it starts refused; over a "
9373
+ "labelled synthetic run the circulating hours, the gallons, the footage and the motor's revolutions equal the hand sums, its no-load "
9374
+ "speed is revolutions per gallon times the flow and the bit's the rotary's plus that, the whirl seven times the rotor's turning, the "
9375
+ "torque estimated from the maker's slope, the one stall found at its second; a motor followed across two runs to its interval"
9376
+ + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
9377
+
9378
+ stab = tr["stability"]
9379
+ tq_rows = [b["torque"] for b in stab["blocks"]]
9380
+ n = 8 * 7200
9381
+ tt = np.arange(n, dtype=float)
9382
+ rng = np.random.default_rng(3)
9383
+ moved = DS._stability(tt, np.ones(n, bool), 9.0 + 0.6 * np.where(tt < n / 2, np.sin(2 * np.pi * 0.10 * tt), np.sin(2 * np.pi * 0.20 * tt)) + rng.normal(0, 0.05, n),
9384
+ None, datetime(2026, 1, 1))
9385
+ walk = DS._stability(tt, np.ones(n, bool), 9.0 + np.cumsum(rng.normal(0, 0.02, n)), None, datetime(2026, 1, 1))
9386
+ short = DS._stability(tt[:3 * 3600], np.ones(3 * 3600, bool), 9.0 + rng.normal(0, 0.05, 3 * 3600), None, datetime(2026, 1, 1))
9387
+ c3 = {
9388
+ "grows": stab["verdict"]["status"] == "CHANGED" and stab["verdict"]["changed"] == ["surface torque oscillation"] and len(stab["blocks"]) == 8
9389
+ and all(abs(r["f_hz"] - 0.1016) < 0.002 for r in tq_rows) and 3.5 < tq_rows[-1]["index"] / tq_rows[0]["index"] < 4.5
9390
+ and abs(tq_rows[0]["index"] - 0.25 / np.sqrt(2) / 9.0) < 0.003,
9391
+ "moves": moved["verdict"]["status"] == "CHANGED" and "surface torque frequency" in moved["verdict"]["changed"]
9392
+ and abs(moved["blocks"][0]["torque"]["f_hz"] - 0.1016) < 0.002 and abs(moved["blocks"][-1]["torque"]["f_hz"] - 0.20) < 0.004,
9393
+ "red_noise": sum(1 for b in walk["blocks"] if b["torque"]["f_hz"]) <= 1,
9394
+ "short": short["verdict"]["status"] == "TOO FEW BLOCKS",
9395
+ }
9396
+ Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer"); Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator")
9397
+ svc = Service(wsp, port=0, scheduler=False).start()
9398
+ base_url = svc.url.rstrip("/")
9399
+
9400
+ def opener(name, pw):
9401
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
9402
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw}).encode())
9403
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
9404
+ return o
9405
+
9406
+ def call(o, method, path, body=None):
9407
+ rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
9408
+ if method == "POST":
9409
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
9410
+ try:
9411
+ with o.open(rq, timeout=120) as rr_:
9412
+ return rr_.status, json.loads(rr_.read())
9413
+ except urllib.error.HTTPError as e:
9414
+ return e.code, json.loads(e.read())
9415
+ try:
9416
+ V, O = opener("v", "viewer-pass-1"), opener("op", "op-pass-123")
9417
+ s_g, pg = call(V, "GET", "/api/drillstring?well=" + w["id"])
9418
+ s_m, mm = call(V, "GET", "/api/tools/motors")
9419
+ s_vp, _ = call(V, "POST", "/api/drillstring/run", {"well_id": w["id"], "name": "x", "components": comps})
9420
+ s_op, rr = call(O, "POST", "/api/drillstring/run", {"well_id": w["id"], "name": "Run 3", "components": comps[:1]})
9421
+ s_bad, _ = call(O, "POST", "/api/drillstring/run", {"well_id": w["id"], "name": "x", "components": [{"class_id": "packer"}]})
9422
+ s_nw, _ = call(O, "POST", "/api/drillstring/run", {"well_id": "nope", "name": "x", "components": comps})
9423
+ s_rm, _ = call(O, "POST", "/api/drillstring/remove", {"run_id": rr.get("id", "")})
9424
+ s_rm2, _ = call(O, "POST", "/api/drillstring/remove", {"run_id": rr.get("id", "")})
9425
+ s_uw, _ = call(V, "GET", "/api/drillstring?well=nope")
9426
+ s_ac, _ = call(O, "POST", "/api/drillstring/nope", {})
9427
+ finally:
9428
+ svc.stop()
9429
+ cli = _sp.run([sys.executable, "-m", "gea", "drillstring", "--workspace", wsp], capture_output=True, text=True, cwd=str(pkg.parent))
9430
+ cli_m = _sp.run([sys.executable, "-m", "gea", "tools", "--action", "motors"], capture_output=True, text=True, cwd=str(pkg.parent))
9431
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
9432
+ c3.update({
9433
+ "api": s_g == 200 and len(pg["runs"]) == 2 and pg["runs"][0]["track"]["usage"]["circulating_hours"] == 16.0 and s_m == 200 and len(mm["motors"]) == 41
9434
+ and s_vp == 403 and s_op == 200 and s_bad == 400 and s_nw == 404 and s_rm == 200 and s_rm2 == 404 and s_uw == 404 and s_ac == 404,
9435
+ "cli": cli.returncode == 0 and "stability: CHANGED" in cli.stdout and "DUE: Positive displacement mud motor" in cli.stdout
9436
+ and cli_m.returncode == 0 and "msd-6-3-4-78-6.4" in cli_m.stdout,
9437
+ "page": all(x in page_src for x in ("VIEWS.drillstring", "['#/drillstring', 'Drill string']", "dsDiagram", "Declare a run", "Oscillation index, block by block",
9438
+ "Distinct oscillation frequency", "id=\"mtCard\"", "/api/tools/motors", "Every component, across every run")),
9439
+ "help": any(t[0] == "drillstring" for t in H.INDEX) and not H.check("drillstring") and H.VIEW_TOPIC.get("drillstring") == "drillstring",
9440
+ })
9441
+ failed3 = [k for k, v in c3.items() if not v]
9442
+ ok(not failed3,
9443
+ "CI3 the stability: a torque oscillation at 0.10 Hz growing fourfold found as a change at its frequency and amplitude, one that moves "
9444
+ "from 0.10 to 0.20 Hz found as moved, red noise rising to the band's edge not called a peak, a short record too few blocks to "
9445
+ "compare; the API (a viewer reads, only an operator declares; a bad class, an unknown well, a second removal and an unknown action "
9446
+ "named), the CLI, the Drill String tab with its build diagram, the motor table on the Tools tab, and the help page"
9447
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
9448
+
9449
+
9140
9450
  def main() -> int:
9141
9451
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
9142
9452
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -9225,6 +9535,8 @@ def main() -> int:
9225
9535
  section_ce_well3d(tmp)
9226
9536
  section_cf_economics(tmp)
9227
9537
  section_cg_well_report(tmp)
9538
+ section_ch_simulation(tmp)
9539
+ section_ci_drill_string(tmp)
9228
9540
  if _FAILS:
9229
9541
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
9230
9542
  for f in _FAILS: