gea-program 0.34.0__tar.gz → 0.36.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 (300) hide show
  1. {gea_program-0.34.0/gea_program.egg-info → gea_program-0.36.0}/PKG-INFO +35 -1
  2. {gea_program-0.34.0 → gea_program-0.36.0}/README.md +34 -0
  3. {gea_program-0.34.0 → gea_program-0.36.0}/gea/__init__.py +1 -1
  4. {gea_program-0.34.0 → gea_program-0.36.0}/gea/__main__.py +105 -0
  5. {gea_program-0.34.0 → gea_program-0.36.0}/gea/acceptance_tests.py +354 -0
  6. gea_program-0.36.0/gea/control_room.py +509 -0
  7. {gea_program-0.34.0 → gea_program-0.36.0}/gea/geodesy.py +1 -0
  8. gea_program-0.36.0/gea/help/control-room.md +11 -0
  9. gea_program-0.36.0/gea/help/well3d.md +11 -0
  10. {gea_program-0.34.0 → gea_program-0.36.0}/gea/helplib.py +3 -1
  11. gea_program-0.36.0/gea/reference/volve_3d.provenance.json +198 -0
  12. gea_program-0.36.0/gea/reference/volve_surfaces.json.gz +0 -0
  13. gea_program-0.36.0/gea/reference/volve_traj_F-10.xml.gz +0 -0
  14. gea_program-0.36.0/gea/reference/volve_traj_F-4.xml.gz +0 -0
  15. gea_program-0.36.0/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  16. gea_program-0.36.0/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  17. gea_program-0.36.0/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  18. gea_program-0.36.0/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  19. gea_program-0.36.0/gea/reference/volve_well_picks.csv.gz +0 -0
  20. {gea_program-0.34.0 → gea_program-0.36.0}/gea/service.py +122 -4
  21. {gea_program-0.34.0 → gea_program-0.36.0}/gea/web/app.html +379 -6
  22. gea_program-0.36.0/gea/well3d.py +688 -0
  23. {gea_program-0.34.0 → gea_program-0.36.0/gea_program.egg-info}/PKG-INFO +35 -1
  24. {gea_program-0.34.0 → gea_program-0.36.0}/gea_program.egg-info/SOURCES.txt +13 -0
  25. {gea_program-0.34.0 → gea_program-0.36.0}/pyproject.toml +1 -1
  26. {gea_program-0.34.0 → gea_program-0.36.0}/LICENSE +0 -0
  27. {gea_program-0.34.0 → gea_program-0.36.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  28. {gea_program-0.34.0 → gea_program-0.36.0}/gea/accuracy_statement.py +0 -0
  29. {gea_program-0.34.0 → gea_program-0.36.0}/gea/alarm_engine.py +0 -0
  30. {gea_program-0.34.0 → gea_program-0.36.0}/gea/backup.py +0 -0
  31. {gea_program-0.34.0 → gea_program-0.36.0}/gea/bench.py +0 -0
  32. {gea_program-0.34.0 → gea_program-0.36.0}/gea/blind_harness.py +0 -0
  33. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  34. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  35. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  36. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  37. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  38. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  39. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  40. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  41. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  42. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  43. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  44. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  45. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  46. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  47. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  48. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  49. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  50. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  51. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  52. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  53. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  54. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  55. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  56. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  57. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  58. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  59. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  60. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  61. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  62. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  63. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  64. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  65. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  66. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  67. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  68. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  69. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  70. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  71. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  72. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  73. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  74. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  75. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  76. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  77. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  78. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  79. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  80. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  81. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  82. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  83. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  84. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  85. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  86. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  87. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  88. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  89. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  90. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  91. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  92. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  93. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  94. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  95. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  96. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  97. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  98. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  99. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  100. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/med_160_sapropels.txt +0 -0
  101. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  102. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  103. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  104. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  105. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  106. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  107. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  108. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  109. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  110. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  111. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  112. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  113. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  114. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  115. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  116. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  117. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  118. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  119. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  120. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  121. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  122. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  123. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  124. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  125. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  126. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  127. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  128. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  129. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  130. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  131. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  132. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  133. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  134. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  135. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  136. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  137. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog_map.py +0 -0
  138. {gea_program-0.34.0 → gea_program-0.36.0}/gea/catalog_register.py +0 -0
  139. {gea_program-0.34.0 → gea_program-0.36.0}/gea/certificates.py +0 -0
  140. {gea_program-0.34.0 → gea_program-0.36.0}/gea/cli.py +0 -0
  141. {gea_program-0.34.0 → gea_program-0.36.0}/gea/client_reports.py +0 -0
  142. {gea_program-0.34.0 → gea_program-0.36.0}/gea/config_versioning.py +0 -0
  143. {gea_program-0.34.0 → gea_program-0.36.0}/gea/conformance.py +0 -0
  144. {gea_program-0.34.0 → gea_program-0.36.0}/gea/correlation.py +0 -0
  145. {gea_program-0.34.0 → gea_program-0.36.0}/gea/dashboard.py +0 -0
  146. {gea_program-0.34.0 → gea_program-0.36.0}/gea/deviation.py +0 -0
  147. {gea_program-0.34.0 → gea_program-0.36.0}/gea/dlis.py +0 -0
  148. {gea_program-0.34.0 → gea_program-0.36.0}/gea/dnp3.py +0 -0
  149. {gea_program-0.34.0 → gea_program-0.36.0}/gea/doctor.py +0 -0
  150. {gea_program-0.34.0 → gea_program-0.36.0}/gea/downhole_engine.py +0 -0
  151. {gea_program-0.34.0 → gea_program-0.36.0}/gea/drift_monitor.py +0 -0
  152. {gea_program-0.34.0 → gea_program-0.36.0}/gea/earth_model.py +0 -0
  153. {gea_program-0.34.0 → gea_program-0.36.0}/gea/edr.py +0 -0
  154. {gea_program-0.34.0 → gea_program-0.36.0}/gea/elevation.py +0 -0
  155. {gea_program-0.34.0 → gea_program-0.36.0}/gea/etp.py +0 -0
  156. {gea_program-0.34.0 → gea_program-0.36.0}/gea/etp12_schemas.json +0 -0
  157. {gea_program-0.34.0 → gea_program-0.36.0}/gea/example_register_map.json +0 -0
  158. {gea_program-0.34.0 → gea_program-0.36.0}/gea/fat_sat.py +0 -0
  159. {gea_program-0.34.0 → gea_program-0.36.0}/gea/files.py +0 -0
  160. {gea_program-0.34.0 → gea_program-0.36.0}/gea/fixtures/README.md +0 -0
  161. {gea_program-0.34.0 → gea_program-0.36.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  162. {gea_program-0.34.0 → gea_program-0.36.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  163. {gea_program-0.34.0 → gea_program-0.36.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  164. {gea_program-0.34.0 → gea_program-0.36.0}/gea/follower.py +0 -0
  165. {gea_program-0.34.0 → gea_program-0.36.0}/gea/forward_model.py +0 -0
  166. {gea_program-0.34.0 → gea_program-0.36.0}/gea/friction_priors.py +0 -0
  167. {gea_program-0.34.0 → gea_program-0.36.0}/gea/gamma.py +0 -0
  168. {gea_program-0.34.0 → gea_program-0.36.0}/gea/gauge_aging.py +0 -0
  169. {gea_program-0.34.0 → gea_program-0.36.0}/gea/gauge_specs.py +0 -0
  170. {gea_program-0.34.0 → gea_program-0.36.0}/gea/gravity_reference.py +0 -0
  171. {gea_program-0.34.0 → gea_program-0.36.0}/gea/hartip.py +0 -0
  172. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/TESTER_GUIDE.md +0 -0
  173. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/alarms.md +0 -0
  174. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/audit-update.md +0 -0
  175. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/backup.md +0 -0
  176. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/catalog.md +0 -0
  177. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/charts.md +0 -0
  178. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/conformance.md +0 -0
  179. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/data-in.md +0 -0
  180. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/dlis.md +0 -0
  181. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/doctor.md +0 -0
  182. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/drift.md +0 -0
  183. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/edr.md +0 -0
  184. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/etp.md +0 -0
  185. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/field-ports.md +0 -0
  186. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/files.md +0 -0
  187. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/historian.md +0 -0
  188. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/instruments.md +0 -0
  189. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/interval.md +0 -0
  190. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/map.md +0 -0
  191. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/month-end.md +0 -0
  192. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/notifications.md +0 -0
  193. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/osdu.md +0 -0
  194. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/patches.md +0 -0
  195. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/ppdm.md +0 -0
  196. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/quakeml.md +0 -0
  197. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/quality.md +0 -0
  198. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/seedlink.md +0 -0
  199. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/seismic.md +0 -0
  200. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/setup.md +0 -0
  201. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/site.md +0 -0
  202. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/sites.md +0 -0
  203. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/slip.md +0 -0
  204. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/soak.md +0 -0
  205. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/sra.md +0 -0
  206. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/start.md +0 -0
  207. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/structure.md +0 -0
  208. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/tools.md +0 -0
  209. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/transients.md +0 -0
  210. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/upkeep.md +0 -0
  211. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/vibration.md +0 -0
  212. {gea_program-0.34.0 → gea_program-0.36.0}/gea/help/well-tests.md +0 -0
  213. {gea_program-0.34.0 → gea_program-0.36.0}/gea/housekeeping.py +0 -0
  214. {gea_program-0.34.0 → gea_program-0.36.0}/gea/interval.py +0 -0
  215. {gea_program-0.34.0 → gea_program-0.36.0}/gea/inverse_engine.py +0 -0
  216. {gea_program-0.34.0 → gea_program-0.36.0}/gea/jobs.py +0 -0
  217. {gea_program-0.34.0 → gea_program-0.36.0}/gea/live_ports.py +0 -0
  218. {gea_program-0.34.0 → gea_program-0.36.0}/gea/loadtest.py +0 -0
  219. {gea_program-0.34.0 → gea_program-0.36.0}/gea/matplotlib_demo.py +0 -0
  220. {gea_program-0.34.0 → gea_program-0.36.0}/gea/modbus.py +0 -0
  221. {gea_program-0.34.0 → gea_program-0.36.0}/gea/modbus_rtu.py +0 -0
  222. {gea_program-0.34.0 → gea_program-0.36.0}/gea/model_card.py +0 -0
  223. {gea_program-0.34.0 → gea_program-0.36.0}/gea/month_end.py +0 -0
  224. {gea_program-0.34.0 → gea_program-0.36.0}/gea/mqtt_port.py +0 -0
  225. {gea_program-0.34.0 → gea_program-0.36.0}/gea/notify.py +0 -0
  226. {gea_program-0.34.0 → gea_program-0.36.0}/gea/opcua_port.py +0 -0
  227. {gea_program-0.34.0 → gea_program-0.36.0}/gea/operator_app.py +0 -0
  228. {gea_program-0.34.0 → gea_program-0.36.0}/gea/osdu.py +0 -0
  229. {gea_program-0.34.0 → gea_program-0.36.0}/gea/patches.py +0 -0
  230. {gea_program-0.34.0 → gea_program-0.36.0}/gea/permits.py +0 -0
  231. {gea_program-0.34.0 → gea_program-0.36.0}/gea/piwebapi.py +0 -0
  232. {gea_program-0.34.0 → gea_program-0.36.0}/gea/ports.py +0 -0
  233. {gea_program-0.34.0 → gea_program-0.36.0}/gea/ppdm.py +0 -0
  234. {gea_program-0.34.0 → gea_program-0.36.0}/gea/profile_catalog.py +0 -0
  235. {gea_program-0.34.0 → gea_program-0.36.0}/gea/project.py +0 -0
  236. {gea_program-0.34.0 → gea_program-0.36.0}/gea/qt6_downhole_app.py +0 -0
  237. {gea_program-0.34.0 → gea_program-0.36.0}/gea/quakeml.py +0 -0
  238. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reconciler.py +0 -0
  239. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  240. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  241. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  242. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  243. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  244. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  245. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/tool_sources.json +0 -0
  246. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/us_outline.json +0 -0
  247. {gea_program-0.34.0 → gea_program-0.36.0}/gea/reference/us_outline.provenance.json +0 -0
  248. {gea_program-0.34.0 → gea_program-0.36.0}/gea/rock_inventory.py +0 -0
  249. {gea_program-0.34.0 → gea_program-0.36.0}/gea/sample_record.py +0 -0
  250. {gea_program-0.34.0 → gea_program-0.36.0}/gea/sample_well_profile.csv +0 -0
  251. {gea_program-0.34.0 → gea_program-0.36.0}/gea/sbom.py +0 -0
  252. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seedlink.py +0 -0
  253. {gea_program-0.34.0 → gea_program-0.36.0}/gea/segy.py +0 -0
  254. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic.py +0 -0
  255. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_array.py +0 -0
  256. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_assoc.py +0 -0
  257. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_detect.py +0 -0
  258. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_film.py +0 -0
  259. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_harmonic.py +0 -0
  260. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_qc.py +0 -0
  261. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_response.py +0 -0
  262. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_signature.py +0 -0
  263. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_track.py +0 -0
  264. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_uncertainty.py +0 -0
  265. {gea_program-0.34.0 → gea_program-0.36.0}/gea/seismic_unlisted.py +0 -0
  266. {gea_program-0.34.0 → gea_program-0.36.0}/gea/sensor_swap.py +0 -0
  267. {gea_program-0.34.0 → gea_program-0.36.0}/gea/series_cache.py +0 -0
  268. {gea_program-0.34.0 → gea_program-0.36.0}/gea/service_life.py +0 -0
  269. {gea_program-0.34.0 → gea_program-0.36.0}/gea/setup_checklist.py +0 -0
  270. {gea_program-0.34.0 → gea_program-0.36.0}/gea/shapefile.py +0 -0
  271. {gea_program-0.34.0 → gea_program-0.36.0}/gea/shell.py +0 -0
  272. {gea_program-0.34.0 → gea_program-0.36.0}/gea/shut_in.py +0 -0
  273. {gea_program-0.34.0 → gea_program-0.36.0}/gea/site_map.py +0 -0
  274. {gea_program-0.34.0 → gea_program-0.36.0}/gea/sla_report.py +0 -0
  275. {gea_program-0.34.0 → gea_program-0.36.0}/gea/slip.py +0 -0
  276. {gea_program-0.34.0 → gea_program-0.36.0}/gea/soak.py +0 -0
  277. {gea_program-0.34.0 → gea_program-0.36.0}/gea/sra.py +0 -0
  278. {gea_program-0.34.0 → gea_program-0.36.0}/gea/store_forward.py +0 -0
  279. {gea_program-0.34.0 → gea_program-0.36.0}/gea/strata_join.py +0 -0
  280. {gea_program-0.34.0 → gea_program-0.36.0}/gea/structure.py +0 -0
  281. {gea_program-0.34.0 → gea_program-0.36.0}/gea/supervisor.py +0 -0
  282. {gea_program-0.34.0 → gea_program-0.36.0}/gea/survey_cmd.py +0 -0
  283. {gea_program-0.34.0 → gea_program-0.36.0}/gea/survey_view.py +0 -0
  284. {gea_program-0.34.0 → gea_program-0.36.0}/gea/telemetry.py +0 -0
  285. {gea_program-0.34.0 → gea_program-0.36.0}/gea/tool_catalog.py +0 -0
  286. {gea_program-0.34.0 → gea_program-0.36.0}/gea/tool_library.py +0 -0
  287. {gea_program-0.34.0 → gea_program-0.36.0}/gea/tool_taxonomy.py +0 -0
  288. {gea_program-0.34.0 → gea_program-0.36.0}/gea/transient.py +0 -0
  289. {gea_program-0.34.0 → gea_program-0.36.0}/gea/vibration.py +0 -0
  290. {gea_program-0.34.0 → gea_program-0.36.0}/gea/well_assembler.py +0 -0
  291. {gea_program-0.34.0 → gea_program-0.36.0}/gea/well_test_validation.py +0 -0
  292. {gea_program-0.34.0 → gea_program-0.36.0}/gea/wits0.py +0 -0
  293. {gea_program-0.34.0 → gea_program-0.36.0}/gea/witsml.py +0 -0
  294. {gea_program-0.34.0 → gea_program-0.36.0}/gea/workspace.py +0 -0
  295. {gea_program-0.34.0 → gea_program-0.36.0}/gea_program.egg-info/dependency_links.txt +0 -0
  296. {gea_program-0.34.0 → gea_program-0.36.0}/gea_program.egg-info/entry_points.txt +0 -0
  297. {gea_program-0.34.0 → gea_program-0.36.0}/gea_program.egg-info/requires.txt +0 -0
  298. {gea_program-0.34.0 → gea_program-0.36.0}/gea_program.egg-info/top_level.txt +0 -0
  299. {gea_program-0.34.0 → gea_program-0.36.0}/setup.cfg +0 -0
  300. {gea_program-0.34.0 → gea_program-0.36.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.34.0
3
+ Version: 0.36.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,40 @@ 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
+ ### The well in 3D
652
+
653
+ The 3D tab draws each well's path from its directional survey by the
654
+ minimum-curvature method, below sea level from its declared rig floor, among
655
+ the mapped formations - interpreted horizon surfaces, or a formation-top
656
+ map's contours at their elevations - with its picks on it. Colour the wells
657
+ by formation, inclination, dogleg severity, or a drilling recorder's gamma,
658
+ rate of penetration or weight on bit along the bit depth; replay the bit
659
+ going down the hole from the recorder's bit depth or the survey stations'
660
+ own times. Turn, pan and zoom by pointer, touch or keys. The Volve field
661
+ (Equinor's open data: three original surveys, the field's formation picks,
662
+ five mapped horizons) ships as the worked example, checked against itself.
663
+ Until then the workspace's formation-top contours are drawn on their own.
664
+ A workspace well is drawn once it has a position, a reference elevation and
665
+ a survey: `gea trajectory` attaches one, verbatim, with its azimuth
666
+ reference stated. `gea well3d`, `gea help well3d`.
667
+
668
+ ### The control room
669
+
670
+ The Control room tab opens the wall: a screen read from across the room, one
671
+ tile per well, worst first, each with its state in a shape and a word -
672
+ Serious, Watch, Good, No data - the reasons in plain words, the alarms, the
673
+ gauges, and for a drilling recorder the bottom, the bit, the rate of
674
+ penetration and the hours on bottom at the end of the record. It refreshes
675
+ itself every 30 seconds, says how old its last answer is, and puts a notice
676
+ across the screen when the service stops answering; a recorded well is
677
+ described at the end of its record and never called stale. F is full screen;
678
+ the screen is kept awake where the browser allows it; when the wells do not
679
+ fit, the pages turn, the worst first. The outgoing operator writes the shift
680
+ handover - what is open, what is being watched, what was done - and it stays
681
+ on the wall until the incoming operator acknowledges it; both go in the audit
682
+ log. A wall pass lets a screen show the wall and nothing else with no one's
683
+ account signed in. `gea wall`, `gea handover`, `gea help control-room`.
684
+
651
685
  ### The site map
652
686
 
653
687
  The Map tab puts everything in the workspace that has a position in one
@@ -609,6 +609,40 @@ 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
+ ### The well in 3D
613
+
614
+ The 3D tab draws each well's path from its directional survey by the
615
+ minimum-curvature method, below sea level from its declared rig floor, among
616
+ the mapped formations - interpreted horizon surfaces, or a formation-top
617
+ map's contours at their elevations - with its picks on it. Colour the wells
618
+ by formation, inclination, dogleg severity, or a drilling recorder's gamma,
619
+ rate of penetration or weight on bit along the bit depth; replay the bit
620
+ going down the hole from the recorder's bit depth or the survey stations'
621
+ own times. Turn, pan and zoom by pointer, touch or keys. The Volve field
622
+ (Equinor's open data: three original surveys, the field's formation picks,
623
+ five mapped horizons) ships as the worked example, checked against itself.
624
+ Until then the workspace's formation-top contours are drawn on their own.
625
+ A workspace well is drawn once it has a position, a reference elevation and
626
+ a survey: `gea trajectory` attaches one, verbatim, with its azimuth
627
+ reference stated. `gea well3d`, `gea help well3d`.
628
+
629
+ ### The control room
630
+
631
+ The Control room tab opens the wall: a screen read from across the room, one
632
+ tile per well, worst first, each with its state in a shape and a word -
633
+ Serious, Watch, Good, No data - the reasons in plain words, the alarms, the
634
+ gauges, and for a drilling recorder the bottom, the bit, the rate of
635
+ penetration and the hours on bottom at the end of the record. It refreshes
636
+ itself every 30 seconds, says how old its last answer is, and puts a notice
637
+ across the screen when the service stops answering; a recorded well is
638
+ described at the end of its record and never called stale. F is full screen;
639
+ the screen is kept awake where the browser allows it; when the wells do not
640
+ fit, the pages turn, the worst first. The outgoing operator writes the shift
641
+ handover - what is open, what is being watched, what was done - and it stays
642
+ on the wall until the incoming operator acknowledges it; both go in the audit
643
+ log. A wall pass lets a screen show the wall and nothing else with no one's
644
+ account signed in. `gea wall`, `gea handover`, `gea help control-room`.
645
+
612
646
  ### The site map
613
647
 
614
648
  The Map tab puts everything in the workspace that has a position in one
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.34.0"
126
+ __version__ = "0.36.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -475,6 +475,38 @@ def main(argv=None) -> int:
475
475
  p_ck.add_argument("--note", type=str, default="", help="--confirm: what it was checked against (required)")
476
476
  p_ck.add_argument("--actor", type=str, default="cli")
477
477
 
478
+ p_tj = sub.add_parser("trajectory", help="attach a well's directional survey (WITSML trajectory or a table of MD, Inc, Azi), kept verbatim beside the well, so the 3D tab can draw its path")
479
+ p_tj.add_argument("--workspace", type=str, required=True)
480
+ p_tj.add_argument("--well", type=str, required=True)
481
+ p_tj.add_argument("--file", type=str, required=True)
482
+ p_tj.add_argument("--azimuth-reference", type=str, required=True, choices=("grid", "true", "magnetic"))
483
+ p_tj.add_argument("--grid", type=str, default=None, help="grid azimuths: the grid, e.g. TX_CENTRAL or UTM14N")
484
+ p_tj.add_argument("--declination", type=float, default=None, help="magnetic azimuths: the declination used, degrees east positive")
485
+ p_tj.add_argument("--md-unit", type=str, default="ft", choices=("ft", "m"))
486
+ p_tj.add_argument("--depth-reference", type=str, default=None, help="KB, DF or GL: must match the well's declared reference elevation")
487
+ p_tj.add_argument("--source", type=str, required=True, help='where the survey came from, e.g. "RRC imaged records, directional survey"')
488
+ p_tj.add_argument("--actor", type=str, default="cli")
489
+
490
+ p_3d = sub.add_parser("well3d", help="the 3D scene in words: each well's path, how it was checked, the surfaces and the replay (the 3D tab draws it)")
491
+ p_3d.add_argument("--scene", type=str, default="volve", choices=("volve", "workspace"))
492
+ p_3d.add_argument("--workspace", type=str, default=None)
493
+ p_3d.add_argument("--json", action="store_true")
494
+
495
+ 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)")
496
+ p_wl.add_argument("--workspace", type=str, default=None, help="the workspace folder, e.g. C:\\site")
497
+ p_wl.add_argument("--json", action="store_true")
498
+
499
+ p_ho = sub.add_parser("handover", help="the shift handover: list the notes, sign one (--write) or acknowledge one (--ack)")
500
+ p_ho.add_argument("--workspace", type=str, default=None, help="the workspace folder, e.g. C:\\site")
501
+ p_ho.add_argument("--write", action="store_true", help="sign a note: give at least one of --open, --watching, --done")
502
+ p_ho.add_argument("--open", type=str, default="", help="--write: what the next shift must pick up")
503
+ p_ho.add_argument("--watching", type=str, default="", help="--write: what is being watched, and why")
504
+ p_ho.add_argument("--done", type=str, default="", help="--write: what this shift did")
505
+ p_ho.add_argument("--ack", type=str, default=None, help="acknowledge the note with this id (not your own)")
506
+ p_ho.add_argument("--comment", type=str, default="", help="--ack: an optional comment")
507
+ p_ho.add_argument("--actor", type=str, default=None, help="who signs or acknowledges (required with --write and --ack)")
508
+ p_ho.add_argument("--json", action="store_true")
509
+
478
510
  p_sv = sub.add_parser("serve", help="serve the dashboard over a workspace: the page, the reports and the JSON API (standard library HTTP)")
479
511
  p_sv.add_argument("--workspace", type=str, required=True, help="the workspace folder (gea workspace --action init)")
480
512
  p_sv.add_argument("--host", type=str, default="127.0.0.1", help="bind address; 0.0.0.0 exposes it on the site network (put TLS in front)")
@@ -1922,6 +1954,79 @@ def main(argv=None) -> int:
1922
1954
  else:
1923
1955
  print(_SC.text(r))
1924
1956
  return 0 if r["ready"] else 2
1957
+ elif a.cmd in ("trajectory", "well3d"):
1958
+ import json as _json
1959
+ from . import well3d as _W3
1960
+ from .cli import _safe_stdout
1961
+ from .workspace import Workspace, WorkspaceError
1962
+ _safe_stdout()
1963
+ ws = None
1964
+ if a.cmd == "trajectory" or a.scene == "workspace":
1965
+ if not a.workspace:
1966
+ raise SystemExit(f"{a.cmd} needs --workspace, e.g. --workspace C:\\site")
1967
+ try:
1968
+ ws = Workspace(a.workspace)
1969
+ except WorkspaceError as e:
1970
+ raise SystemExit(f"{a.cmd}: {e}")
1971
+ if a.cmd == "trajectory":
1972
+ try:
1973
+ with open(a.file, "rb") as f:
1974
+ content = f.read()
1975
+ r = _W3.attach_trajectory(ws, a.well, a.file, content, a.actor, a.azimuth_reference, a.md_unit, a.depth_reference, a.grid,
1976
+ a.declination, a.source)
1977
+ except (ValueError, OSError, WorkspaceError) as e:
1978
+ raise SystemExit(f"trajectory: {e}")
1979
+ print(f"{a.well}: {r['stations']} stations, {r['md_range_m'][0]:,.1f} to {r['md_range_m'][1]:,.1f} m MD, azimuths {r['azimuth_reference']}"
1980
+ + (f" ({r['grid']})" if r.get('grid') else "") + f"; kept as wells/{a.well}/trajectory/{r['file']}")
1981
+ return 0
1982
+ sc = _W3.scene(ws, a.scene)
1983
+ print(_json.dumps(sc, indent=1) if a.json else _W3.text(sc))
1984
+ return 0
1985
+ elif a.cmd in ("wall", "handover"):
1986
+ import json as _json
1987
+ from . import control_room as _CR
1988
+ from .cli import _safe_stdout
1989
+ from .workspace import Workspace, WorkspaceError
1990
+ _safe_stdout()
1991
+ if not a.workspace:
1992
+ raise SystemExit(f"{a.cmd} needs --workspace, e.g. --workspace C:\\site")
1993
+ try:
1994
+ ws = Workspace(a.workspace)
1995
+ except WorkspaceError as e:
1996
+ raise SystemExit(f"{a.cmd}: {e}")
1997
+ if a.cmd == "wall":
1998
+ b = _CR.board(ws)
1999
+ print(_json.dumps(b, indent=1) if a.json else _CR.text(b))
2000
+ return 0
2001
+ try:
2002
+ if a.write:
2003
+ if not a.actor:
2004
+ raise SystemExit("handover --write needs --actor, the name the note is signed with")
2005
+ b = _CR.board(ws)
2006
+ counts = {**b["counts"], "alarms_unacked": sum(((t.get("alarms") or {}).get("unacked") or 0) for t in b["tiles"])}
2007
+ n = _CR.write_note(ws, a.actor, a.open, a.watching, a.done, counts)
2008
+ print(f"handover {n['id']} signed by {n['by']} at {n['utc']}")
2009
+ return 0
2010
+ if a.ack:
2011
+ if not a.actor:
2012
+ raise SystemExit("handover --ack needs --actor, the incoming operator's name")
2013
+ r = _CR.ack_note(ws, a.ack, a.actor, a.comment)
2014
+ print(f"handover {a.ack} acknowledged by {r['by']} at {r['utc']}")
2015
+ return 0
2016
+ except (ValueError, KeyError, PermissionError) as e:
2017
+ raise SystemExit(f"handover: {e.args[0] if e.args else e}")
2018
+ ns = _CR.notes(ws, 0)
2019
+ if a.json:
2020
+ print(_json.dumps(ns, indent=1))
2021
+ elif not ns:
2022
+ print("no handover notes yet")
2023
+ else:
2024
+ for n in ns:
2025
+ print(f"{n['id']} {n['utc']} by {n['by']} " + (f"acknowledged by {n['ack']['by']} at {n['ack']['utc']}" if n.get('ack') else "NOT YET ACKNOWLEDGED"))
2026
+ for k in _CR.NOTE_FIELDS:
2027
+ if n.get(k):
2028
+ print(f" {k}: {n[k]}")
2029
+ return 0
1925
2030
  elif a.cmd == "serve":
1926
2031
  from .service import Service
1927
2032
  from .workspace import WorkspaceError
@@ -8448,6 +8448,358 @@ def section_cc_site_map(tmp: str) -> None:
8448
8448
  "bar, the not-placed wells with the way to place them, what is at a click; the help page"
8449
8449
  + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
8450
8450
 
8451
+
8452
+ def section_cd_control_room(tmp: str) -> None:
8453
+ """Section CD - the control room: the wall board judged by its stated rule (a shape and a word for each state, worst
8454
+ first, a recorded well never called stale, a live feed's age measured), the shift handover signed by one operator and
8455
+ acknowledged by another with both in the audit log, wall passes that show the wall and nothing else, the page."""
8456
+ import http.cookiejar
8457
+ import urllib.request
8458
+ import urllib.error
8459
+ from datetime import datetime as _dt, timedelta as _td, timezone as _tz
8460
+ from . import control_room as CR
8461
+ from . import edr as E
8462
+ from . import helplib as H
8463
+ from .workspace import Workspace
8464
+ from .service import Service, Users
8465
+ pkg = Path(__file__).parent
8466
+ d = Path(tmp, "cd"); d.mkdir()
8467
+
8468
+ ev = E.selftest_events(str(d))
8469
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
8470
+ wd = ws.add_well_file(ev["path"], display="Drilling well", actor="tester")
8471
+ wa = ws.add_well_file(ev["path"], display="Alarm well", actor="tester")
8472
+ wc = ws.add_well_catalog("volve_f12_f14_production_excerpt", "15/9-F-12", 10000.0, actor="tester")
8473
+ ws.refresh_dashboard(actor="tester")
8474
+ wn = ws.add_well_file(ev["path"], display="New well", actor="tester") # added after the refresh: no reports yet
8475
+ ra = Path(ws.reports_dir, "wells", wa["id"])
8476
+ (ra / "alarm_event_report.json").write_text(json.dumps({"active": [
8477
+ {"alarm_id": "SPP.HH_RANGE", "state": "ACTIVE_UNACKED", "priority": "P2", "tag_id": "SPP"},
8478
+ {"alarm_id": "HKLD.QUALITY", "state": "ACTIVE_ACKED", "priority": "P4", "tag_id": "HKLD"}], "shelved": []}), encoding="utf-8")
8479
+ now = _dt(2026, 10, 15, 12, 0, 0, tzinfo=_tz.utc)
8480
+ iso = lambda s: (now - _td(seconds=s)).strftime("%Y-%m-%dT%H:%M:%SZ")
8481
+ feed = lambda age, status="CONNECTED", wid=wn["id"]: [{"name": "p1", "well_id": wid, "status": status, "last_sample_utc": (iso(age) if age is not None else None), "patch": {"stale_after_s": 120.0}}]
8482
+ b0 = CR.board(ws, [], now=now)
8483
+ t0 = {t["well_id"]: t for t in b0["tiles"]}
8484
+ dr = t0[wd["id"]]["drilling"]
8485
+ live = {k: {t["well_id"]: t for t in CR.board(ws, feed(*a), now=now)["tiles"]}[wn["id"]] for k, a in
8486
+ {"fresh": (30,), "late": (400,), "silent": (1000,), "down": (60, "DOWN"), "none": (None,)}.items()}
8487
+ c1 = {
8488
+ "states": t0[wa["id"]]["state"] == "SERIOUS" and t0[wa["id"]]["reasons"][:2] == ["1 unacknowledged P2 alarm(s)", "1 other active alarm(s)"]
8489
+ and t0[wc["id"]]["state"] == "SERIOUS" and "no explanation" in t0[wc["id"]]["reasons"][0]
8490
+ and t0[wd["id"]]["state"] == "GOOD" and t0[wd["id"]]["reasons"] == ["nothing needs attention"]
8491
+ and t0[wn["id"]]["state"] == "NO_DATA",
8492
+ "shape_word": all(t["shape"] == CR.SHAPE[t["state"]] and t["word"] == CR.WORD[t["state"]] for t in b0["tiles"])
8493
+ and len(set(CR.SHAPE.values())) == 4 and len(set(CR.WORD.values())) == 4,
8494
+ "order": [t["state"] for t in b0["tiles"]] == sorted((t["state"] for t in b0["tiles"]), key=CR.STATES.index)
8495
+ and b0["counts"] == {"SERIOUS": 2, "WATCH": 0, "GOOD": 1, "NO_DATA": 1},
8496
+ "recorded": t0[wd["id"]]["live"] is None and t0[wd["id"]]["recorded"]["record_end_utc"] == dr["record_end_utc"]
8497
+ and "never called stale" in b0["rule"],
8498
+ "read_only": not Path(wsp, "handover").exists() and not Path(wsp, "wall").exists(),
8499
+ "drilling": dr["cache"] and dr["td_md_ft"] == 5750.0 and dr["bottom_ft"] == 5750.0 and dr["on_bottom_hours"] is not None
8500
+ and (dr["rop_ft_h"] is not None) == bool(dr["drilling_now"]),
8501
+ "live": live["fresh"]["state"] == "GOOD" and live["late"]["state"] == "WATCH" and "past the 2 min stale limit" in live["late"]["reasons"][0]
8502
+ and live["silent"]["state"] == "SERIOUS" and live["silent"]["reasons"][0].startswith("no live sample for 16 min")
8503
+ and live["down"]["state"] == "SERIOUS" and live["none"]["state"] == "WATCH" and abs(live["fresh"]["live"]["age_s"] - 30) < 1e-6,
8504
+ }
8505
+ failed1 = [k for k, v in c1.items() if not v]
8506
+ ok(not failed1,
8507
+ "CD1 the wall board by its stated rule, read without writing anything: an unacknowledged P2 alarm and a gauge off with no explanation SERIOUS, a well with reports and "
8508
+ "nothing else GOOD, a well with neither reports nor a live sample NO DATA, each state its own shape and word, worst first; a recorded well "
8509
+ "described at the end of its record and never called stale (bottom 5,750 ft, the ROP only while drilling at the end); a live feed's "
8510
+ "measured age against its stale limit - 30 s good, 400 s watch, 1,000 s serious, a patch down serious"
8511
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
8512
+
8513
+ users = Users(str(Path(wsp, "users.json")))
8514
+ for n_, r_ in (("adm", "admin"), ("op1", "operator"), ("op2", "operator"), ("v", "viewer")):
8515
+ users.add(n_, f"{n_}-pass-123", r_)
8516
+ svc = Service(wsp, port=0, scheduler=False).start()
8517
+ base_url = svc.url.rstrip("/")
8518
+
8519
+ def opener():
8520
+ return urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
8521
+
8522
+ def call(op, method, path, body=None):
8523
+ req = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
8524
+ if method == "POST":
8525
+ req.add_header("Content-Type", "application/json"); req.add_header("X-GEA-Action", "1")
8526
+ try:
8527
+ with op.open(req, timeout=120) as rr_:
8528
+ return rr_.status, json.loads(rr_.read())
8529
+ except urllib.error.HTTPError as e:
8530
+ return e.code, json.loads(e.read())
8531
+
8532
+ def signin(name):
8533
+ op = opener(); call(op, "POST", "/api/login", {"name": name, "password": f"{name}-pass-123"}); return op
8534
+ try:
8535
+ anon = opener()
8536
+ st_anon, _ = call(anon, "GET", "/api/wall")
8537
+ V, O1, O2, A = signin("v"), signin("op1"), signin("op2"), signin("adm")
8538
+ st_v, bv = call(V, "GET", "/api/wall")
8539
+ st_vw, _ = call(V, "POST", "/api/handover", {"open": "x"})
8540
+ st_empty, _ = call(O1, "POST", "/api/handover", {"open": " ", "watching": "", "done": ""})
8541
+ st_long, _ = call(O1, "POST", "/api/handover", {"open": "x" * 2001})
8542
+ st_w, note = call(O1, "POST", "/api/handover", {"open": "Pump 2 liner swap not finished.", "watching": "SPP on the alarm well.", "done": "Reamed to bottom."})
8543
+ _, bdg1 = call(V, "GET", "/api/badges")
8544
+ _, wall1 = call(V, "GET", "/api/wall")
8545
+ st_self, self_ = call(O1, "POST", "/api/handover/ack", {"id": note.get("id")})
8546
+ st_ack, ack = call(O2, "POST", "/api/handover/ack", {"id": note.get("id"), "comment": "read at 06:05"})
8547
+ st_ack2, _ = call(O2, "POST", "/api/handover/ack", {"id": note.get("id")})
8548
+ st_nf, _ = call(O2, "POST", "/api/handover/ack", {"id": "H-none"})
8549
+ _, bdg2 = call(V, "GET", "/api/badges")
8550
+ _, hv = call(V, "GET", "/api/handover")
8551
+ # wall passes
8552
+ st_pv, _ = call(V, "POST", "/api/wall/pass", {"label": "doghouse"})
8553
+ st_pnl, _ = call(A, "POST", "/api/wall/pass", {"label": " "})
8554
+ st_pc, wp = call(A, "POST", "/api/wall/pass", {"label": "doghouse wall", "days": 30})
8555
+ _, plist = call(A, "GET", "/api/wall/passes")
8556
+ W = opener()
8557
+ st_bad, _ = call(W, "POST", "/api/wall/enter", {"code": wp["id"] + ".wrong"})
8558
+ st_in, _ = call(W, "POST", "/api/wall/enter", {"code": wp["code"]})
8559
+ st_ww, wwall = call(W, "GET", "/api/wall")
8560
+ _, wsess = call(W, "GET", "/api/session")
8561
+ st_wo, _ = call(W, "GET", "/api/overview")
8562
+ st_wh, _ = call(W, "GET", "/api/handover")
8563
+ st_wpost, _ = call(W, "POST", "/api/handover", {"open": "x"})
8564
+ st_rv, _ = call(A, "POST", "/api/wall/pass/revoke", {"id": wp["id"]})
8565
+ st_after, _ = call(W, "GET", "/api/wall")
8566
+ _, plist2 = call(A, "GET", "/api/wall/passes")
8567
+ L = opener()
8568
+ lock = [call(L, "POST", "/api/wall/enter", {"code": "W000000.nope"})[0] for _ in range(6)]
8569
+ finally:
8570
+ svc.stop()
8571
+ audit = [e["action"] for e in ws.audit_log(5000)]
8572
+ raw = (Path(wsp, "wall", "passes.json")).read_text(encoding="utf-8")
8573
+ lines = Path(wsp, "handover", "notes.jsonl").read_text(encoding="utf-8").splitlines()
8574
+ c2 = {
8575
+ "roles": st_anon == 401 and st_v == 200 and len(bv["tiles"]) == 4 and st_vw == 403,
8576
+ "note": st_empty == 400 and st_long == 400 and st_w == 200 and note["by"] == "op1" and note["ack"] is None
8577
+ and note["board"]["SERIOUS"] == 2 and note["board"]["alarms_unacked"] == 1
8578
+ and wall1["handover"]["id"] == note["id"] and wall1["handover"]["ack"] is None and bdg1["handover_unacked"] == 1,
8579
+ "ack": st_self == 403 and "author cannot acknowledge" in self_["error"] and st_ack == 200 and ack["by"] == "op2"
8580
+ and st_ack2 == 400 and st_nf == 404 and bdg2["handover_unacked"] == 0
8581
+ and hv["notes"][0]["ack"]["by"] == "op2" and hv["notes"][0]["ack"]["comment"] == "read at 06:05",
8582
+ "append_only": len(lines) == 2 and json.loads(lines[0])["kind"] == "note" and json.loads(lines[1])["kind"] == "ack",
8583
+ "audit": all(a_ in audit for a_ in ("handover.write", "handover.ack", "wall.pass.create", "wall.pass.failed", "wall.pass.enter", "wall.pass.revoke")),
8584
+ }
8585
+ failed2 = [k for k, v in c2.items() if not v]
8586
+ ok(not failed2,
8587
+ "CD2 the shift handover: a viewer reads the wall and cannot write; an empty or over-long note refused; a signed note keeps its author, its "
8588
+ "time and the board's counts (2 serious, 1 unacknowledged alarm), shows on the wall and badges the tab until acknowledged; the author "
8589
+ "cannot acknowledge it, the next operator can (once, with a comment); notes and acknowledgements appended, never rewritten; every step in "
8590
+ "the audit log"
8591
+ + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
8592
+
8593
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
8594
+ help_md = (pkg / "help" / "control-room.md").read_text(encoding="utf-8")
8595
+ c3 = {
8596
+ "pass": st_pv == 403 and st_pnl == 400 and st_pc == 200 and wp["code"].startswith(wp["id"] + ".") and wp["code"].split(".", 1)[1] not in raw
8597
+ and "hash" not in json.dumps(plist) and plist["passes"][0]["status"] == "ACTIVE",
8598
+ "wall_only": st_bad == 401 and st_in == 200 and st_ww == 200 and len(wwall["tiles"]) == 4 and wsess["user"] is None
8599
+ and wsess["wall_pass"]["label"] == "doghouse wall" and st_wo == 401 and st_wh == 401 and st_wpost == 401,
8600
+ "revoke": st_rv == 200 and st_after == 401 and plist2["passes"][0]["status"] == "REVOKED" and lock[-1] == 429,
8601
+ "page": all(x in page_src for x in ("VIEWS.wall", "VIEWS.control", "['#/control', 'Control room']", "/api/wall", "/api/handover/ack",
8602
+ "NOT UPDATING", "wakeLock", "requestFullscreen", "SIGNED OUT", "This is a wall screen",
8603
+ "crShape", "data-age0", "turns pages", "/api/wall/pass/revoke", "handover_unacked")),
8604
+ "help": any(t["topic"] == "control-room" for t in H.topics()) and not H.check("control-room") and H.VIEW_TOPIC.get("control") == "control-room"
8605
+ and "never called stale" in help_md.split("will not call a measurement")[1],
8606
+ }
8607
+ failed3 = [k for k, v in c3.items() if not v]
8608
+ ok(not failed3,
8609
+ "CD3 wall passes: issued by an administrator only and named for the screen, the code shown once and only its hash kept; a wrong pass "
8610
+ "refused and locked out after repeats; a pass shows the wall and nothing else - no other page, no handover list, no writing - until it is "
8611
+ "revoked; the page: the wall full screen with its shapes and words, the age of its last answer, NOT UPDATING across the screen when the "
8612
+ "service stops, the screen kept awake, pages turned when the wells do not fit, the handover written and acknowledged; the help page"
8613
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
8614
+
8615
+
8616
+
8617
+ def section_ce_well3d(tmp: str) -> None:
8618
+ """Section CE - the well in 3D: the minimum-curvature method against a known arc and against the survey companies'
8619
+ own positions, the shipped Volve excerpt checked against itself (frame, picks, surfaces, provenance), a workspace
8620
+ well drawn from an attached survey among its formation-top contours with its recorder replay, the API and the tab."""
8621
+ import base64 as _b64
8622
+ import gzip as _gz
8623
+ import hashlib as _hl
8624
+ import http.cookiejar
8625
+ import math as _m
8626
+ import subprocess as _sp
8627
+ import urllib.request
8628
+ import urllib.error
8629
+ from . import edr as E
8630
+ from . import geodesy as G
8631
+ from . import helplib as H
8632
+ from . import shapefile as SF
8633
+ from . import well3d as W
8634
+ from .workspace import Workspace
8635
+ from .service import Service, Users
8636
+ pkg = Path(__file__).parent
8637
+ d = Path(tmp, "ce"); d.mkdir()
8638
+
8639
+ R = 1000.0
8640
+ q = W.min_curvature([0.0, _m.pi * R / 2], [0.0, 90.0], [0.0, 0.0])
8641
+ p_arc = W.Path([0.0, _m.pi * R / 2], [0.0, 90.0], [0.0, 0.0], 0.0, 0.0, 0.0)
8642
+ mid = p_arc.at(_m.pi * R / 4)
8643
+ tr4 = W.read_witsml_trajectory(_gz.decompress((pkg / "reference" / "volve_traj_F-4.xml.gz").read_bytes()).decode())[0]
8644
+ tr10 = W.read_witsml_trajectory(_gz.decompress((pkg / "reference" / "volve_traj_F-10.xml.gz").read_bytes()).decode())[0]
8645
+ f5 = [t for n in ("36in", "17.5in", "12.25in", "8.5in") for t in W.read_witsml_trajectory(_gz.decompress((pkg / "reference" / f"volve_traj_F-5_{n}.xml.gz").read_bytes()).decode())]
8646
+ rep5, _ = W.replay_from_stations(f5)
8647
+ rep4, note4 = W.replay_from_stations([tr4])
8648
+ 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"
8649
+ refused = []
8650
+ for bad in ("Depth,Angle\n0,0\n", "MD,Inc,Azi\n0,0,0\n0,1,2\n", "MD,Inc,Azi\n0,0,0\n100,x,2\n", "MD,Inc,Azi\n0,0,0\n100,200,2\n"):
8651
+ try:
8652
+ W.read_survey_csv(bad)
8653
+ except ValueError:
8654
+ refused.append(True)
8655
+ st_csv = W.read_survey_csv(csv_ok, "ft")
8656
+ c1 = {
8657
+ "arc": abs(q["n"][1] - R) < 1e-9 and abs(q["v"][1] - R) < 1e-9 and abs(mid[1] - R * (1 - _m.cos(_m.pi / 4))) < 1e-6
8658
+ and abs(mid[2] - R * _m.sin(_m.pi / 4)) < 1e-6 and abs(q["dls"][1] - 90.0 / (_m.pi * R / 2) * 30.0) < 1e-9,
8659
+ "units": tr4["azi_ref"] == "true north" and abs(tr4["stations"][40]["incl"] - _m.degrees(0.446106147512686)) < 1e-9 and tr10["grid_cor_deg"] == -0.95
8660
+ and len(tr4["stations"]) == 87 and abs(st_csv[-1]["md"] - 5750 * 0.3048) < 1e-9,
8661
+ "company": all(x is not None and x < 0.01 for x in (W.file_position_miss(tr4["stations"]), W.file_position_miss(tr10["stations"]),
8662
+ W.file_position_miss(f5[-1]["stations"]))),
8663
+ "csv": len(refused) == 4,
8664
+ "replay": rep5 is not None and len(rep5["t"]) == 100 and rep5["t"][0].startswith("2007-11-16") and rep5["t"][-1].startswith("2008-07-29")
8665
+ and rep4 is None and "1 distinct time" in note4 and all(m0 <= m1 or t0 < t1 for (t0, m0), (t1, m1) in zip(zip(rep5["t"], rep5["md"]), zip(rep5["t"][1:], rep5["md"][1:]))),
8666
+ }
8667
+ failed1 = [k for k, v in c1.items() if not v]
8668
+ ok(not failed1,
8669
+ "CE1 the minimum-curvature method: a quarter circle of 1,000 m radius ends 1,000 m out and 1,000 m down, and its middle lies on the arc "
8670
+ "(292.9 m out, 707.1 m down), not on a chord; the WITSML reader takes radians and degrees alike; the paths computed from the Volve files "
8671
+ "reproduce the TVD and displacements the survey companies wrote in them to under a centimetre; a survey table without its columns, with a "
8672
+ "repeated depth or a bad row refused; the replay only from stations measured while drilling (F-5: 100 steps, Nov 2007 to Jul 2008; F-4's "
8673
+ "one-time resurvey not replayed)"
8674
+ + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
8675
+
8676
+ sc = W.volve_scene()
8677
+ wl = {w["id"]: w for w in sc["wells"]}
8678
+ picks = W.volve_picks()
8679
+ dms = lambda dd, mm, ss: dd + mm / 60 + ss / 3600
8680
+ frame = []
8681
+ for wn, la, lo in (("NO 15/9-F-1 C", dms(58, 26, 29.907), dms(1, 53, 14.708)), ("NO 15/9-F-11 B", dms(58, 26, 29.960), dms(1, 53, 14.870))):
8682
+ sb = next(p for p in picks if p["WELL"] == wn and p["PICKS"] == "Seabed")
8683
+ a = G.unproject(float(sb["EASTING"]), float(sb["NORTHING"]), G.utm(31, True, "INTL1924"))
8684
+ b = G.unproject(float(sb["EASTING"]), float(sb["NORTHING"]), G.utm(31, True, "WGS84"))
8685
+ frame.append((G.geodesic_m(a[0], a[1], la, lo)["distance_m"], G.geodesic_m(b[0], b[1], la, lo)["distance_m"]))
8686
+ prov = json.loads((pkg / "reference" / "volve_3d.provenance.json").read_text(encoding="utf-8"))
8687
+ hashes_ok = all(_hl.sha256((pkg / "reference" / k).read_bytes()).hexdigest() == v["sha256"] for k, v in prov["outputs"].items())
8688
+ pts = prov["outputs"]["volve_surfaces.json.gz"]["points"]
8689
+ # the repository stores every text file with LF line ends (.gitattributes: text=auto eol=lf), so a package file holding
8690
+ # CRLF would reach a checkout - and the published wheel - as different bytes from the ones hashed here
8691
+ crlf = []
8692
+ for dp, dn, fn in os.walk(pkg):
8693
+ dn[:] = [x for x in dn if x not in ("__pycache__", "catalog_operator")]
8694
+ for f_ in fn:
8695
+ b_ = Path(dp, f_).read_bytes()
8696
+ if b"\0" not in b_[:8000] and b"\r\n" in b_:
8697
+ crlf.append(os.path.relpath(os.path.join(dp, f_), pkg))
8698
+ c2 = {
8699
+ "frame": all(a < 0.1 and 140 < b < 160 for a, b in frame) and "ED50" in sc["frame"]["crs"],
8700
+ "picks": all(wl[k]["pick_miss_max_m"] < 0.3 and abs(wl[k]["ref_elev_m"] - 54.9) < 1e-6 for k in ("F-4", "F-5", "F-10"))
8701
+ and abs(wl["F-10"]["td_md"] - 5300.07) < 0.01 and len(sc["picks_only"]) == 31,
8702
+ "azimuth": wl["F-4"]["azimuth"]["applied"] == "as grid" and "14 m" in wl["F-4"]["azimuth"]["note"] and wl["F-5"]["azimuth"]["file_reference"] == "grid north",
8703
+ "surfaces": len(sc["surfaces"]) == 5 and all(s["affine_miss_m"] < 0.01 and s["tie_median_m"] < 7.0 and len(s["ties"]) >= 23 for s in sc["surfaces"])
8704
+ and all(pts[s["id"]] == [s["n_source_points"], s["n_points"]] for s in sc["surfaces"])
8705
+ and [s["id"] for s in sc["surfaces"]] == ["Ty", "SHETLAND", "BCU", "Hugin_Fm_Top", "Hugin_Base"],
8706
+ "line_ends": not crlf and all(k.endswith(".gz") or b"\r\n" not in (pkg / "reference" / k).read_bytes() for k in prov["outputs"]),
8707
+ "provenance": hashes_ok and "commercial purposes" in prov["license"] and "may not be sold" in prov["license"] and len(prov["sources"]) == 12
8708
+ and all(len(v["commit"]) == 40 for v in prov["sources"].values()) and "Equinor ASA" in sc["attribution"] and "not sold" in sc["attribution"],
8709
+ }
8710
+ failed2 = [k for k, v in c2.items() if not v]
8711
+ ok(not failed2,
8712
+ "CE2 the Volve excerpt checked against itself: its eastings and northings are ED50 zone 31N (the slots unproject within 0.1 m of the "
8713
+ "operator's own log headers, about 150 m off if read as WGS84); the three paths land on Equinor's picks to under 0.3 m from a 54.9 m rig "
8714
+ "floor; 15/9-F-4's azimuths read as grid, with the 14 m it would miss by otherwise stated; five surfaces drawn as delivered, every fourth "
8715
+ "line, each within a few metres of its picks at the median; every shipped file hashed in its provenance with the pinned public copy it "
8716
+ "came from (and no package file with CRLF line ends, which a checkout would change from the bytes hashed), the licence (commercial use, no sale) and the attribution on the page"
8717
+ + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
8718
+
8719
+ ev = E.selftest_events(str(d))
8720
+ wsp = str(d / "ws"); ws = Workspace.create(wsp, "Pad 3", actor="tester")
8721
+ wb = ws.add_well_file(ev["path"], display="Well B", actor="tester"); wc = ws.add_well_file(ev["path"], display="Well C", actor="tester")
8722
+ 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")
8723
+ lat1, lon1 = 31.42, -104.10
8724
+ kmlon = 1.0 / (111.195 * _m.cos(_m.radians(lat1)))
8725
+ SF.write(str(d / "Top_A"), "PolyLine", [([[(lon1 + (i - 1.5) * kmlon + 0.0005 * k, lat1 - 0.05 + 0.001 * k) for k in range(100)]], {"ElevTop_ft": -1500.0 - 100.0 * i}) for i in range(4)],
8726
+ [("ElevTop_ft", "F", 10, 1)], SF.NAD83_GEOGCS)
8727
+ SF.write(str(d / "Faults"), "PolyLine", [([[(lon1 - 0.02 + 0.001 * j, lat1) for j in range(40)]], {"TYPE": "normal fault"})], [("TYPE", "C", 20, 0)], SF.NAD83_GEOGCS)
8728
+ ws.add_layer(str(d / "Top_A.shp"), "Top A", actor="tester", formation="A", role="top", citation="test")
8729
+ ws.add_layer(str(d / "Faults.shp"), "Faults", actor="tester", citation="test")
8730
+ sc0 = W.workspace_scene(ws) # no survey yet: the formation tops alone
8731
+ att_refused = 0
8732
+ for kw in ({"azimuth_reference": ""}, {"azimuth_reference": "grid"}, {"azimuth_reference": "magnetic"}, {"azimuth_reference": "true", "source": " "}):
8733
+ k = {"azimuth_reference": "true", "source": "s", **kw}
8734
+ try:
8735
+ W.attach_trajectory(ws, wb["id"], "s.csv", csv_ok.encode(), "tester", k["azimuth_reference"], "ft", None, None, None, k["source"])
8736
+ except ValueError:
8737
+ att_refused += 1
8738
+ ws.refresh_dashboard(actor="tester")
8739
+ Users(str(Path(wsp, "users.json"))).add("v", "viewer-pass-1", "viewer"); Users(str(Path(wsp, "users.json"))).add("op", "op-pass-123", "operator")
8740
+ svc = Service(wsp, port=0, scheduler=False).start()
8741
+ base_url = svc.url.rstrip("/")
8742
+
8743
+ def opener(name, pw):
8744
+ o = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
8745
+ rq = urllib.request.Request(base_url + "/api/login", method="POST", data=json.dumps({"name": name, "password": pw}).encode())
8746
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1"); o.open(rq, timeout=30)
8747
+ return o
8748
+
8749
+ def call(o, method, path, body=None):
8750
+ rq = urllib.request.Request(base_url + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
8751
+ if method == "POST":
8752
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
8753
+ try:
8754
+ with o.open(rq, timeout=120) as rr_:
8755
+ return rr_.status, json.loads(rr_.read())
8756
+ except urllib.error.HTTPError as e:
8757
+ return e.code, json.loads(e.read())
8758
+ try:
8759
+ V, O = opener("v", "viewer-pass-1"), opener("op", "op-pass-123")
8760
+ st_l, ls = call(V, "GET", "/api/3d/scenes")
8761
+ st_v, vs = call(V, "GET", "/api/3d/scene/volve")
8762
+ st_n, _ = call(V, "GET", "/api/3d/scene/nope")
8763
+ body = {"filename": "survey.csv", "content_b64": _b64.b64encode(csv_ok.encode()).decode(), "azimuth_reference": "grid", "grid": "TX_CENTRAL",
8764
+ "md_unit": "ft", "source": "test survey"}
8765
+ st_pv, _ = call(V, "POST", f"/api/wells/{wb['id']}/trajectory", body)
8766
+ st_b64, _ = call(O, "POST", f"/api/wells/{wb['id']}/trajectory", {**body, "content_b64": "%%%"})
8767
+ st_at, at = call(O, "POST", f"/api/wells/{wb['id']}/trajectory", body)
8768
+ st_w, wsc = call(V, "GET", "/api/3d/scene/workspace")
8769
+ finally:
8770
+ svc.stop()
8771
+ kept = (Path(wsp, "wells", wb["id"], "trajectory", "survey.csv").read_bytes() == csv_ok.encode())
8772
+ audit = [e["action"] for e in ws.audit_log(500)]
8773
+ cli = _sp.run([sys.executable, "-m", "gea", "well3d", "--scene", "workspace", "--workspace", wsp], capture_output=True, text=True, cwd=str(pkg.parent))
8774
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
8775
+ help_md = (pkg / "help" / "well3d.md").read_text(encoding="utf-8")
8776
+ w0 = (wsc.get("wells") or [{}])[0]
8777
+ c3 = {
8778
+ "attach": att_refused == 4 and st_pv == 403 and st_b64 == 400 and st_at == 200 and at["stations"] == 6 and at["grid"] == "TX_CENTRAL" and kept
8779
+ and at["sha256"] == _hl.sha256(csv_ok.encode()).hexdigest() and "well.trajectory" in audit,
8780
+ "scene": st_l == 200 and [s["id"] for s in ls["scenes"]] == ["workspace", "volve"] and st_v == 200 and len(vs["wells"]) == 3 and st_n == 404
8781
+ and st_w == 200 and len(wsc["wells"]) == 1 and abs(w0["td_md"] - 1752.6) < 0.01 and "TX_CENTRAL grid north" in w0["azimuth"]["applied"]
8782
+ and w0["replay"]["kind"] == "drilling recorder" and "Gamma" in w0["channels"] and not w0["replay"]["t"][0].endswith("Z")
8783
+ and [n["id"] for n in wsc["not_placed"]] == [wc["id"]] and wsc["layers"][0]["lines"][0]["z"] == round(1500 * 0.3048, 1)
8784
+ and any(n["display"] == "Faults" and "no depth" in n["why"] for n in wsc["layers_not_drawn"]),
8785
+ "tops_alone": sc0["wells"] == [] and [l["name"] for l in sc0["layers"]] == ["Top A"] and sc0["frame"]["area"] == "the placed wells"
8786
+ and {n["id"]: n["why"] for n in sc0["not_placed"]}[wb["id"]] == "no directional survey attached",
8787
+ "cli": cli.returncode == 0 and "Well B: 6 stations" in cli.stdout and "contour lines at their elevations" in cli.stdout,
8788
+ "page": all(x in page_src for x in ("VIEWS['3d']", "['#/3d', '3D']", "getContext('webgl'", "OES_element_index_uint", "w3Tube", "w3SurfMesh",
8789
+ "/api/3d/scene/", "w3Play", "the recorder's clock", "ArrowLeft", "see-through", "/trajectory`", "w3Hit")),
8790
+ "help": any(t["topic"] == "well3d" for t in H.topics()) and not H.check("well3d") and H.VIEW_TOPIC.get("3d") == "well3d"
8791
+ and "a well with no survey" in help_md.split("will not call a measurement")[1],
8792
+ }
8793
+ failed3 = [k for k, v in c3.items() if not v]
8794
+ ok(not failed3,
8795
+ "CE3 a workspace well in 3D: a survey attached only with its azimuth reference stated (a grid named, a declination given) and its source, "
8796
+ "kept byte for byte and audited; the path turned from TX_CENTRAL grid north by the convergence there, set below sea level from the "
8797
+ "declared rig floor among the formation-top contours at their elevations (a fault trace listed as having no depth; the tops drawn alone "
8798
+ "before any survey), the recorder's bit depth replayed on its own clock and its gamma along the path; a well without a position listed, not drawn; the API, the CLI and the 3D "
8799
+ "tab (WebGL by hand, the replay, turn and zoom by pointer and keys); the help page"
8800
+ + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
8801
+
8802
+
8451
8803
  def main() -> int:
8452
8804
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
8453
8805
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -8532,6 +8884,8 @@ def main() -> int:
8532
8884
  section_ca_setup_checklist(tmp)
8533
8885
  section_cb_charts(tmp)
8534
8886
  section_cc_site_map(tmp)
8887
+ section_cd_control_room(tmp)
8888
+ section_ce_well3d(tmp)
8535
8889
  if _FAILS:
8536
8890
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
8537
8891
  for f in _FAILS: