gea-program 0.8.0__tar.gz → 0.9.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 (227) hide show
  1. {gea_program-0.8.0 → gea_program-0.9.0}/PKG-INFO +25 -2
  2. {gea_program-0.8.0 → gea_program-0.9.0}/README.md +24 -1
  3. {gea_program-0.8.0 → gea_program-0.9.0}/gea/__init__.py +1 -1
  4. {gea_program-0.8.0 → gea_program-0.9.0}/gea/__main__.py +68 -1
  5. {gea_program-0.8.0 → gea_program-0.9.0}/gea/acceptance_tests.py +176 -21
  6. {gea_program-0.8.0 → gea_program-0.9.0}/gea/client_reports.py +78 -0
  7. gea_program-0.9.0/gea/help/sra.md +15 -0
  8. {gea_program-0.8.0 → gea_program-0.9.0}/gea/helplib.py +1 -0
  9. {gea_program-0.8.0 → gea_program-0.9.0}/gea/jobs.py +15 -6
  10. {gea_program-0.8.0 → gea_program-0.9.0}/gea/service.py +13 -1
  11. gea_program-0.9.0/gea/sra.py +613 -0
  12. {gea_program-0.8.0 → gea_program-0.9.0}/gea/web/app.html +3 -3
  13. {gea_program-0.8.0 → gea_program-0.9.0}/gea/workspace.py +115 -0
  14. {gea_program-0.8.0 → gea_program-0.9.0}/gea_program.egg-info/PKG-INFO +25 -2
  15. {gea_program-0.8.0 → gea_program-0.9.0}/gea_program.egg-info/SOURCES.txt +2 -0
  16. {gea_program-0.8.0 → gea_program-0.9.0}/pyproject.toml +1 -1
  17. {gea_program-0.8.0 → gea_program-0.9.0}/LICENSE +0 -0
  18. {gea_program-0.8.0 → gea_program-0.9.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  19. {gea_program-0.8.0 → gea_program-0.9.0}/gea/accuracy_statement.py +0 -0
  20. {gea_program-0.8.0 → gea_program-0.9.0}/gea/alarm_engine.py +0 -0
  21. {gea_program-0.8.0 → gea_program-0.9.0}/gea/bench.py +0 -0
  22. {gea_program-0.8.0 → gea_program-0.9.0}/gea/blind_harness.py +0 -0
  23. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  24. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  25. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  26. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  27. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  28. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  29. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  30. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  31. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  32. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  33. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  34. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  35. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  36. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  37. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  38. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  39. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  40. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  41. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  42. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  43. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  44. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  45. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  46. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  47. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  48. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  49. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  50. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  51. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  52. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  53. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  54. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  55. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  56. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  57. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  58. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  59. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  60. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  61. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  62. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  63. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  64. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  65. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  66. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  67. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  68. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  69. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  70. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  71. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  72. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  73. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  74. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  75. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  76. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  77. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  78. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  79. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  80. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  81. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  82. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  83. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  84. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  85. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  86. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  87. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  88. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  89. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  90. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/med_160_sapropels.txt +0 -0
  91. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  92. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  93. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  94. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  95. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  96. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  97. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  98. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  99. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  100. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  101. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  102. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  103. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  104. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  105. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  106. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  107. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  108. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  109. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  110. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  111. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  112. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  113. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  114. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  115. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  116. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  117. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  118. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  119. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  120. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  121. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  122. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  123. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  124. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  125. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  126. {gea_program-0.8.0 → gea_program-0.9.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  127. {gea_program-0.8.0 → gea_program-0.9.0}/gea/certificates.py +0 -0
  128. {gea_program-0.8.0 → gea_program-0.9.0}/gea/cli.py +0 -0
  129. {gea_program-0.8.0 → gea_program-0.9.0}/gea/config_versioning.py +0 -0
  130. {gea_program-0.8.0 → gea_program-0.9.0}/gea/correlation.py +0 -0
  131. {gea_program-0.8.0 → gea_program-0.9.0}/gea/dashboard.py +0 -0
  132. {gea_program-0.8.0 → gea_program-0.9.0}/gea/deviation.py +0 -0
  133. {gea_program-0.8.0 → gea_program-0.9.0}/gea/doctor.py +0 -0
  134. {gea_program-0.8.0 → gea_program-0.9.0}/gea/downhole_engine.py +0 -0
  135. {gea_program-0.8.0 → gea_program-0.9.0}/gea/drift_monitor.py +0 -0
  136. {gea_program-0.8.0 → gea_program-0.9.0}/gea/earth_model.py +0 -0
  137. {gea_program-0.8.0 → gea_program-0.9.0}/gea/example_register_map.json +0 -0
  138. {gea_program-0.8.0 → gea_program-0.9.0}/gea/fat_sat.py +0 -0
  139. {gea_program-0.8.0 → gea_program-0.9.0}/gea/files.py +0 -0
  140. {gea_program-0.8.0 → gea_program-0.9.0}/gea/follower.py +0 -0
  141. {gea_program-0.8.0 → gea_program-0.9.0}/gea/forward_model.py +0 -0
  142. {gea_program-0.8.0 → gea_program-0.9.0}/gea/gamma.py +0 -0
  143. {gea_program-0.8.0 → gea_program-0.9.0}/gea/gauge_aging.py +0 -0
  144. {gea_program-0.8.0 → gea_program-0.9.0}/gea/gauge_specs.py +0 -0
  145. {gea_program-0.8.0 → gea_program-0.9.0}/gea/geodesy.py +0 -0
  146. {gea_program-0.8.0 → gea_program-0.9.0}/gea/gravity_reference.py +0 -0
  147. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/TESTER_GUIDE.md +0 -0
  148. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/alarms.md +0 -0
  149. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/audit-update.md +0 -0
  150. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/data-in.md +0 -0
  151. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/doctor.md +0 -0
  152. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/drift.md +0 -0
  153. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/files.md +0 -0
  154. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/instruments.md +0 -0
  155. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/notifications.md +0 -0
  156. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/patches.md +0 -0
  157. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/quality.md +0 -0
  158. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/seismic.md +0 -0
  159. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/site.md +0 -0
  160. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/sites.md +0 -0
  161. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/start.md +0 -0
  162. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/transients.md +0 -0
  163. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/upkeep.md +0 -0
  164. {gea_program-0.8.0 → gea_program-0.9.0}/gea/help/well-tests.md +0 -0
  165. {gea_program-0.8.0 → gea_program-0.9.0}/gea/housekeeping.py +0 -0
  166. {gea_program-0.8.0 → gea_program-0.9.0}/gea/inverse_engine.py +0 -0
  167. {gea_program-0.8.0 → gea_program-0.9.0}/gea/live_ports.py +0 -0
  168. {gea_program-0.8.0 → gea_program-0.9.0}/gea/loadtest.py +0 -0
  169. {gea_program-0.8.0 → gea_program-0.9.0}/gea/matplotlib_demo.py +0 -0
  170. {gea_program-0.8.0 → gea_program-0.9.0}/gea/modbus.py +0 -0
  171. {gea_program-0.8.0 → gea_program-0.9.0}/gea/model_card.py +0 -0
  172. {gea_program-0.8.0 → gea_program-0.9.0}/gea/mqtt_port.py +0 -0
  173. {gea_program-0.8.0 → gea_program-0.9.0}/gea/notify.py +0 -0
  174. {gea_program-0.8.0 → gea_program-0.9.0}/gea/opcua_port.py +0 -0
  175. {gea_program-0.8.0 → gea_program-0.9.0}/gea/operator_app.py +0 -0
  176. {gea_program-0.8.0 → gea_program-0.9.0}/gea/patches.py +0 -0
  177. {gea_program-0.8.0 → gea_program-0.9.0}/gea/permits.py +0 -0
  178. {gea_program-0.8.0 → gea_program-0.9.0}/gea/ports.py +0 -0
  179. {gea_program-0.8.0 → gea_program-0.9.0}/gea/profile_catalog.py +0 -0
  180. {gea_program-0.8.0 → gea_program-0.9.0}/gea/project.py +0 -0
  181. {gea_program-0.8.0 → gea_program-0.9.0}/gea/qt6_downhole_app.py +0 -0
  182. {gea_program-0.8.0 → gea_program-0.9.0}/gea/reconciler.py +0 -0
  183. {gea_program-0.8.0 → gea_program-0.9.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  184. {gea_program-0.8.0 → gea_program-0.9.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  185. {gea_program-0.8.0 → gea_program-0.9.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  186. {gea_program-0.8.0 → gea_program-0.9.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  187. {gea_program-0.8.0 → gea_program-0.9.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  188. {gea_program-0.8.0 → gea_program-0.9.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  189. {gea_program-0.8.0 → gea_program-0.9.0}/gea/rock_inventory.py +0 -0
  190. {gea_program-0.8.0 → gea_program-0.9.0}/gea/sample_record.py +0 -0
  191. {gea_program-0.8.0 → gea_program-0.9.0}/gea/sample_well_profile.csv +0 -0
  192. {gea_program-0.8.0 → gea_program-0.9.0}/gea/sbom.py +0 -0
  193. {gea_program-0.8.0 → gea_program-0.9.0}/gea/segy.py +0 -0
  194. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic.py +0 -0
  195. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_array.py +0 -0
  196. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_detect.py +0 -0
  197. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_film.py +0 -0
  198. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_harmonic.py +0 -0
  199. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_qc.py +0 -0
  200. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_response.py +0 -0
  201. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_signature.py +0 -0
  202. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_track.py +0 -0
  203. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_uncertainty.py +0 -0
  204. {gea_program-0.8.0 → gea_program-0.9.0}/gea/seismic_unlisted.py +0 -0
  205. {gea_program-0.8.0 → gea_program-0.9.0}/gea/sensor_swap.py +0 -0
  206. {gea_program-0.8.0 → gea_program-0.9.0}/gea/service_life.py +0 -0
  207. {gea_program-0.8.0 → gea_program-0.9.0}/gea/shell.py +0 -0
  208. {gea_program-0.8.0 → gea_program-0.9.0}/gea/shut_in.py +0 -0
  209. {gea_program-0.8.0 → gea_program-0.9.0}/gea/sla_report.py +0 -0
  210. {gea_program-0.8.0 → gea_program-0.9.0}/gea/store_forward.py +0 -0
  211. {gea_program-0.8.0 → gea_program-0.9.0}/gea/strata_join.py +0 -0
  212. {gea_program-0.8.0 → gea_program-0.9.0}/gea/supervisor.py +0 -0
  213. {gea_program-0.8.0 → gea_program-0.9.0}/gea/survey_cmd.py +0 -0
  214. {gea_program-0.8.0 → gea_program-0.9.0}/gea/survey_view.py +0 -0
  215. {gea_program-0.8.0 → gea_program-0.9.0}/gea/telemetry.py +0 -0
  216. {gea_program-0.8.0 → gea_program-0.9.0}/gea/tool_library.py +0 -0
  217. {gea_program-0.8.0 → gea_program-0.9.0}/gea/transient.py +0 -0
  218. {gea_program-0.8.0 → gea_program-0.9.0}/gea/well_assembler.py +0 -0
  219. {gea_program-0.8.0 → gea_program-0.9.0}/gea/well_test_validation.py +0 -0
  220. {gea_program-0.8.0 → gea_program-0.9.0}/gea/wits0.py +0 -0
  221. {gea_program-0.8.0 → gea_program-0.9.0}/gea/witsml.py +0 -0
  222. {gea_program-0.8.0 → gea_program-0.9.0}/gea_program.egg-info/dependency_links.txt +0 -0
  223. {gea_program-0.8.0 → gea_program-0.9.0}/gea_program.egg-info/entry_points.txt +0 -0
  224. {gea_program-0.8.0 → gea_program-0.9.0}/gea_program.egg-info/requires.txt +0 -0
  225. {gea_program-0.8.0 → gea_program-0.9.0}/gea_program.egg-info/top_level.txt +0 -0
  226. {gea_program-0.8.0 → gea_program-0.9.0}/setup.cfg +0 -0
  227. {gea_program-0.8.0 → gea_program-0.9.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.8.0
3
+ Version: 0.9.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
@@ -125,7 +125,7 @@ gea dashboard --catalog-well volve_f12_f14_production_excerpt:15/9-F-12:10000 --
125
125
  gea client-report --report accuracy --out client_report
126
126
  gea model-cards --out model_cards
127
127
  gea sbom --out sbom
128
- gea accept # the product gate (302 checks)
128
+ gea accept # the product gate (308 checks)
129
129
  gea help drift # the help library, by the job (16 pages; the same text is on every dashboard page)
130
130
  gea guide # the click-by-click tester guide (docs/TESTER_GUIDE.md)
131
131
  gea gui # the desktop window (pip install "gea-program[desktop]")
@@ -528,6 +528,29 @@ so one geophone can still say when each rig was working - and the Seismic
528
528
  Station Report and the station page carry the section, with the tracks drawn
529
529
  against time.
530
530
 
531
+ ### The Seismicity Response Area packet
532
+
533
+ The Railroad Commission of Texas declares a Seismicity Response Area around a
534
+ cluster of felt earthquakes and expects the operators inside it to run a
535
+ response plan. The plans on file have one shape - a circle of about 9 km, a
536
+ plan written against M 3.5, an 18-month goal, a response group meeting
537
+ within 48 hours of a threshold event, quarterly checkpoints with Commission
538
+ staff, disposal wells in two tiers by the base of the Wolfcamp - and the
539
+ Commission's December 2023 Notice to Operators names the data: four daily
540
+ parameters (maximum and average surface injection pressure, injection
541
+ volume, maximum injection rate) and three bottomhole-pressure methods, one
542
+ of which is a permanent downhole probe. `gea/sra.py` builds the packet to
543
+ that shape from what the site holds. The operator declares what the program
544
+ cannot measure (`set-well`: surface position and datum, API and UIC, depth
545
+ tier, which channels are the pressure, the rate and the downhole gauge, the
546
+ BHP method); `sra-define` puts the area on the site; `sra-report --catalog
547
+ texnet.csv` writes the packet - membership by geodesic distance, the daily
548
+ record per well rolled up by month, the catalogue's events against the plan
549
+ with the trigger and the goal clock, the schedule, and every gap by name -
550
+ and a daily export under the Notice's own parameter names. It will not
551
+ invent a volume for a well with no rate channel, a tier from depth alone, or
552
+ an aftershock: those are declarations, and the packet says so.
553
+
531
554
  ### Audit / Update
532
555
 
533
556
  The Audit / Update tab holds three things. The whole audit log
@@ -86,7 +86,7 @@ gea dashboard --catalog-well volve_f12_f14_production_excerpt:15/9-F-12:10000 --
86
86
  gea client-report --report accuracy --out client_report
87
87
  gea model-cards --out model_cards
88
88
  gea sbom --out sbom
89
- gea accept # the product gate (302 checks)
89
+ gea accept # the product gate (308 checks)
90
90
  gea help drift # the help library, by the job (16 pages; the same text is on every dashboard page)
91
91
  gea guide # the click-by-click tester guide (docs/TESTER_GUIDE.md)
92
92
  gea gui # the desktop window (pip install "gea-program[desktop]")
@@ -489,6 +489,29 @@ so one geophone can still say when each rig was working - and the Seismic
489
489
  Station Report and the station page carry the section, with the tracks drawn
490
490
  against time.
491
491
 
492
+ ### The Seismicity Response Area packet
493
+
494
+ The Railroad Commission of Texas declares a Seismicity Response Area around a
495
+ cluster of felt earthquakes and expects the operators inside it to run a
496
+ response plan. The plans on file have one shape - a circle of about 9 km, a
497
+ plan written against M 3.5, an 18-month goal, a response group meeting
498
+ within 48 hours of a threshold event, quarterly checkpoints with Commission
499
+ staff, disposal wells in two tiers by the base of the Wolfcamp - and the
500
+ Commission's December 2023 Notice to Operators names the data: four daily
501
+ parameters (maximum and average surface injection pressure, injection
502
+ volume, maximum injection rate) and three bottomhole-pressure methods, one
503
+ of which is a permanent downhole probe. `gea/sra.py` builds the packet to
504
+ that shape from what the site holds. The operator declares what the program
505
+ cannot measure (`set-well`: surface position and datum, API and UIC, depth
506
+ tier, which channels are the pressure, the rate and the downhole gauge, the
507
+ BHP method); `sra-define` puts the area on the site; `sra-report --catalog
508
+ texnet.csv` writes the packet - membership by geodesic distance, the daily
509
+ record per well rolled up by month, the catalogue's events against the plan
510
+ with the trigger and the goal clock, the schedule, and every gap by name -
511
+ and a daily export under the Notice's own parameter names. It will not
512
+ invent a volume for a well with no rate channel, a tier from depth alone, or
513
+ an aftershock: those are declarations, and the packet says so.
514
+
492
515
  ### Audit / Update
493
516
 
494
517
  The Audit / Update tab holds three things. The whole audit log
@@ -122,7 +122,7 @@ from .profile_catalog import (
122
122
  read_pangaea_txt,
123
123
  )
124
124
 
125
- __version__ = "0.8.0"
125
+ __version__ = "0.9.0"
126
126
  __all__ = [
127
127
  "aging_rate", "rate_psi_yr", "accuracy_psi",
128
128
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -281,9 +281,33 @@ def main(argv=None) -> int:
281
281
  p_ws.add_argument("--action", type=str, default="list",
282
282
  choices=["init", "add-file", "add-catalog", "add-live", "remove", "list", "migrate", "refresh", "audit",
283
283
  "add-seismic", "refresh-seismic", "remove-seismic", "sar-film", "refresh-all", "add-track", "refresh-track", "remove-track",
284
- "seismic-dataset", "seismic-field", "add-site", "sites", "site-report", "remove-site", "rename"])
284
+ "seismic-dataset", "seismic-field", "add-site", "sites", "site-report", "remove-site", "rename",
285
+ "set-well", "sra-define", "sra-report"])
285
286
  p_ws.add_argument("--stations", type=str, nargs="+", default=None, help="add-track: two or more array station ids of this workspace")
286
287
  p_ws.add_argument("--site", type=str, default=None, help="site-report/remove-site: the site id")
288
+ p_ws.add_argument("--api", type=str, default=None, help="set-well: the API number")
289
+ p_ws.add_argument("--uic", type=str, default=None, help="set-well: the UIC permit number")
290
+ p_ws.add_argument("--depth-tier", type=str, choices=["shallow", "deep"], default=None, help="set-well: above or below the named formation base")
291
+ p_ws.add_argument("--formation", type=str, default=None, help="set-well: the basis of the tier, e.g. 'completed below the base of the Wolfcamp'")
292
+ p_ws.add_argument("--channel-pressure", type=str, default=None, help="set-well: the channel that is the surface injection pressure (psi)")
293
+ p_ws.add_argument("--channel-rate", type=str, default=None, help="set-well: the channel that is the injection rate")
294
+ p_ws.add_argument("--rate-unit", type=str, default=None, help="set-well: the rate channel's unit: bbl/min or bbl/d")
295
+ p_ws.add_argument("--bhp-method", type=str, choices=["calculated", "dip_in", "probe"], default=None, help="set-well: how bottomhole pressure is known")
296
+ p_ws.add_argument("--channel-bhp", type=str, default=None, help="set-well: the downhole gauge channel (probe method)")
297
+ p_ws.add_argument("--top-interval-ft", type=float, default=None, help="set-well: top of the disposal interval, ft (calculated method)")
298
+ p_ws.add_argument("--gradient-psi-ft", type=float, default=None, help="set-well: fluid gradient, psi/ft (calculated method)")
299
+ p_ws.add_argument("--radius-km", type=float, default=None, help="sra-define: the area's radius in km (Gardendale: 9.08)")
300
+ p_ws.add_argument("--plan-date", type=str, default=None, help="sra-define: the plan date, YYYY-MM-DD")
301
+ p_ws.add_argument("--threshold-m", type=float, default=3.5, help="sra-define: the magnitude the plan is written against (3.5)")
302
+ p_ws.add_argument("--goal-months", type=int, default=18, help="sra-define: months without a threshold event that meet the goal (18)")
303
+ p_ws.add_argument("--response-hours", type=int, default=48, help="sra-define: hours within which the response group meets (48)")
304
+ p_ws.add_argument("--checkpoint-months", type=int, default=3, help="sra-define: months between checkpoints with Commission staff (3)")
305
+ p_ws.add_argument("--formation-boundary", type=str, default="base of the Wolfcamp", help="sra-define: the stratigraphic boundary between the two tiers")
306
+ p_ws.add_argument("--triggering-event", type=str, default="", help="sra-define: the catalogue id of the event the plan answers")
307
+ p_ws.add_argument("--catalog", type=str, default=None, help="sra-report: a TexNet earthquake-catalogue export (CSV)")
308
+ p_ws.add_argument("--aftershocks", type=str, nargs="*", default=None, help="sra-report: catalogue ids the operator declares as aftershocks (exempt)")
309
+ p_ws.add_argument("--start", type=str, default=None, help="sra-report: first day of the record, YYYY-MM-DD")
310
+ p_ws.add_argument("--end", type=str, default=None, help="sra-report: last day of the record, YYYY-MM-DD")
287
311
  p_ws.add_argument("--wells", type=str, nargs="+", default=None, help="add-site: well ids of this workspace")
288
312
  p_ws.add_argument("--seismic", type=str, nargs="+", default=None, help="add-site: seismic station ids of this workspace")
289
313
  p_ws.add_argument("--tracks", type=str, nargs="+", default=None, help="add-site: track ids of this workspace")
@@ -362,6 +386,9 @@ def main(argv=None) -> int:
362
386
  p_w0.add_argument("--seed", type=int, default=1)
363
387
  p_w0.add_argument("--connect", type=str, default=None, help="host:port - instead of listening, connect to a listening tap and push frames")
364
388
 
389
+ p_sra = sub.add_parser("sra", help="the Seismicity Response Area packet: the self-test on a labelled scene")
390
+ p_sra.add_argument("--action", choices=["selftest"], default="selftest")
391
+ p_sra.add_argument("--json", action="store_true")
365
392
  p_dr = sub.add_parser("doctor", help="which code is running and can it serve: Python, the package, duplicates, the page, PyPI; with --workspace also the site folder and the port")
366
393
  p_dr.add_argument("--workspace", type=str, default=None)
367
394
  p_dr.add_argument("--host", type=str, default="127.0.0.1")
@@ -1443,6 +1470,18 @@ def main(argv=None) -> int:
1443
1470
  return 0
1444
1471
  print(f"notifications: {'configured' if n.cfg else 'not configured'}" + (f" - {len(n.cfg['channels'])} channel(s), {len(n.cfg['rules'])} rule(s), quiet {n.cfg['quiet_s']} s" if n.cfg else " (gea notify --example)"))
1445
1472
  return 0
1473
+ elif a.cmd == "sra":
1474
+ from . import sra as _SRA
1475
+ r = _SRA.selftest()
1476
+ if a.json:
1477
+ print(json.dumps({k: v for k, v in r.items() if k != "packet"}, indent=1))
1478
+ else:
1479
+ print(f"sra selftest [{r['label']}]: {r['status']}")
1480
+ for k, v in r["checks"].items():
1481
+ print(f" {'ok ' if v else 'BAD'} {k}")
1482
+ print(f" deep well volume {r['deep_mean_volume_bbl_day']} bbl/day; probe offset {r['probe_offset_psi']} psi")
1483
+ print(_SRA.report_text(r["packet"]))
1484
+ return 0 if r["status"] == "OK" else 1
1446
1485
  elif a.cmd == "doctor":
1447
1486
  from .doctor import run as _doctor
1448
1487
  return _doctor(a.workspace, a.host, a.port, a.json)
@@ -1630,6 +1669,34 @@ def main(argv=None) -> int:
1630
1669
  if not a.site:
1631
1670
  raise SystemExit("remove-site needs --site <id>")
1632
1671
  ws.remove_site(a.site, actor=a.actor); print("removed (folder kept):", a.site); return 0
1672
+ elif a.action == "set-well":
1673
+ if not a.well:
1674
+ raise SystemExit("set-well needs --well <id>")
1675
+ surface = {"lat": a.lat, "lon": a.lon, "datum": a.datum} if (a.lat is not None or a.lon is not None) else None
1676
+ disposal = {k: v for k, v in {"api_number": a.api, "uic_number": a.uic, "depth_tier": a.depth_tier, "formation_basis": a.formation,
1677
+ "channel_pressure": a.channel_pressure, "channel_rate": a.channel_rate, "rate_unit": a.rate_unit,
1678
+ "bhp_method": a.bhp_method, "channel_bhp": a.channel_bhp, "top_interval_ft": a.top_interval_ft,
1679
+ "gradient_psi_ft": a.gradient_psi_ft}.items() if v is not None}
1680
+ if not surface and not disposal:
1681
+ raise SystemExit("set-well: nothing to declare (give --lat/--lon, or any of --api --uic --depth-tier --channel-pressure ...)")
1682
+ w = ws.set_well(a.well, surface=surface, disposal=disposal or None, actor=a.actor)
1683
+ print(f"well {a.well}: surface {w.get('surface')}; disposal {w.get('disposal')}"); return 0
1684
+ elif a.action == "sra-define":
1685
+ if not a.site or not a.name or a.lat is None or a.lon is None or a.radius_km is None or not a.plan_date:
1686
+ raise SystemExit("sra-define needs --site, --name, --lat, --lon, --radius-km and --plan-date")
1687
+ sra = ws.sra_define(a.site, actor=a.actor, name=a.name, lat=a.lat, lon=a.lon, radius_km=a.radius_km, plan_date=a.plan_date,
1688
+ datum=a.datum or "WGS84", threshold_m=a.threshold_m, goal_months=a.goal_months, response_hours=a.response_hours,
1689
+ checkpoint_months=a.checkpoint_months, formation_boundary=a.formation_boundary,
1690
+ triggering_event=a.triggering_event, note=a.note or "")
1691
+ print(f"sra: {sra['name']} on site {a.site} - {sra['radius_km']} km around {sra['centre']['lat']}, {sra['centre']['lon']} "
1692
+ f"({sra['centre']['datum']}); plan {sra['plan_date']}; M {sra['threshold_m']}; goal {sra['goal_months']} months"); return 0
1693
+ elif a.action == "sra-report":
1694
+ from .sra import report_text as _sra_text
1695
+ if not a.site:
1696
+ raise SystemExit("sra-report needs --site <id>")
1697
+ r = ws.write_sra_packet(a.site, catalog_csv=a.catalog, aftershocks=a.aftershocks, start=a.start, end=a.end, actor=a.actor)
1698
+ print(_sra_text(r["packet"]))
1699
+ print(" report:", r["paths"]["html"]); print(" daily export:", r["paths"]["daily_csv"], f"({r['export_rows']} rows)"); return 0
1633
1700
  elif a.action == "seismic-dataset":
1634
1701
  r = ws.write_seismic_dataset_report(actor=a.actor)
1635
1702
  inv = r["inventory"]
@@ -2914,7 +2914,8 @@ def section_am_report_samples(tmp: str) -> None:
2914
2914
  "model_card_well_baseline.html", "model_card_well_test_detector.html", "model_card_rock_density_inventory.html",
2915
2915
  "model_card_strata_property_estimator.html", "gauge_drift_report_monitored_SYNTHETIC.html", "alarm_event_report_SYNTHETIC.html",
2916
2916
  "data_resilience_report_SYNTHETIC.html", "sla_report_SYNTHETIC.html", "sat_protocol.html", "seismic_station_report_SYNTHETIC.html", "seismic_track_report_SYNTHETIC.html",
2917
- "seismic_dataset_report_SYNTHETIC.html", "seismic_field_report_SYNTHETIC.html", "site_report_SYNTHETIC.html"}
2917
+ "seismic_dataset_report_SYNTHETIC.html", "seismic_field_report_SYNTHETIC.html", "site_report_SYNTHETIC.html",
2918
+ "sra_packet_SYNTHETIC.html"}
2918
2919
  have = {p.name for p in d.glob("*.html")}
2919
2920
  md = (d / "SAMPLES.md").read_text(encoding="utf-8") if (d / "SAMPLES.md").exists() else ""
2920
2921
  texts = {n: (d / n).read_text(encoding="utf-8", errors="replace") for n in sorted(have & want)}
@@ -4430,27 +4431,51 @@ def section_bb_supervisor(tmp: str) -> None:
4430
4431
  "happens to share a recycled process id is still reported, and a process id that is not the last start's excludes nothing")
4431
4432
 
4432
4433
  # -- the exit code reaches the launcher, and the launcher acts on it -------------------
4433
- spec = importlib.util.spec_from_file_location("bb_installer", str(pkg.parent / "tools" / "build_installer.py"))
4434
- bi = importlib.util.module_from_spec(spec); spec.loader.exec_module(bi)
4435
- cmd = bi.WIN_SCRIPTS["start-dashboard.cmd"]; sh = bi.LINUX_SCRIPTS["start-dashboard.sh"]
4436
- body = [ln for ln in cmd.strip().splitlines() if ln.strip() and not ln.strip().lower().startswith("rem ")]
4437
- labels = set(_re.findall(r"^:(\w+)", cmd, _re.M)); gotos = set(_re.findall(r"goto\s+(\w+)", cmd))
4438
- ok("title GEA - Operations Control Panel" in cmd
4439
- and '"%GEA_RC%"=="86"' in cmd and "goto gea_run" in cmd and not gotos - labels
4440
- and body[-1].lower().startswith("powershell") and "-NoExit" in body[-1] and "start-dashboard.cmd" in body[-1]
4441
- and "python" not in body[-1].lower().split("write-host")[0]
4442
- and 'rc" = "86"' in sh and "continue" in sh and sh.count("exit \"$rc\"") == 1,
4443
- "BB3 the Windows launcher names the window, runs the panel again when it exits with 86, resolves every label it jumps to, and ends by handing "
4444
- "the window to a PowerShell prompt that says how to start the panel again - it never falls back to the shell underneath, which is how an "
4445
- "operator was left at a Python prompt; the shell launcher does the same with a loop")
4446
- ok("auto_restart.flag" in cmd and "auto_restart.flag" in sh
4447
- and "GEA_FAILS" in cmd and "GTR 5" in cmd and "fails" in sh and "-le 5" in sh
4448
- and SV.AUTO_MAX == 5
4449
- and cmd.splitlines()[cmd.splitlines().index(":gea_read_flag") + 1].strip().startswith("set /p GEA_AUTO=<")
4450
- and cmd.count("set /p GEA_AUTO=<") == 1,
4434
+ # In a checkout the launchers are the templates in tools/build_installer.py. In an installed kit there
4435
+ # is no tools/ - the launcher under test is the one the kit installed beside its python/, found the way
4436
+ # doctor finds it. (The v0.8.0 kit gate failed on GitHub reading tools/ from site-packages.)
4437
+ from .doctor import check_launcher, check_workspace, kit_dir
4438
+ cmd = sh = None
4439
+ tools_py = pkg.parent / "tools" / "build_installer.py"
4440
+ if (pkg.parent / "pyproject.toml").exists() and tools_py.exists():
4441
+ spec = importlib.util.spec_from_file_location("bb_installer", str(tools_py))
4442
+ bi = importlib.util.module_from_spec(spec); spec.loader.exec_module(bi)
4443
+ cmd = bi.WIN_SCRIPTS["start-dashboard.cmd"]; sh = bi.LINUX_SCRIPTS["start-dashboard.sh"]; where = "the checkout's templates"
4444
+ else:
4445
+ kd = kit_dir(); where = f"the installed kit at {kd}"
4446
+ if kd and os.path.isfile(os.path.join(kd, "start-dashboard.cmd")):
4447
+ cmd = open(os.path.join(kd, "start-dashboard.cmd"), encoding="utf-8", errors="replace").read()
4448
+ if kd and os.path.isfile(os.path.join(kd, "start-dashboard.sh")):
4449
+ sh = open(os.path.join(kd, "start-dashboard.sh"), encoding="utf-8", errors="replace").read()
4450
+ cmd_ok = sh_ok = True
4451
+ if cmd is not None:
4452
+ body = [ln for ln in cmd.strip().splitlines() if ln.strip() and not ln.strip().lower().startswith("rem ")]
4453
+ labels = set(_re.findall(r"^:(\w+)", cmd, _re.M)); gotos = set(_re.findall(r"goto\s+(\w+)", cmd))
4454
+ cmd_ok = ("title GEA - Operations Control Panel" in cmd
4455
+ and '"%GEA_RC%"=="86"' in cmd and "goto gea_run" in cmd and not gotos - labels
4456
+ and body[-1].lower().startswith("powershell") and "-NoExit" in body[-1] and "start-dashboard.cmd" in body[-1]
4457
+ and "python" not in body[-1].lower().split("write-host")[0])
4458
+ if sh is not None:
4459
+ sh_ok = 'rc" = "86"' in sh and "continue" in sh and sh.count("exit \"$rc\"") == 1
4460
+ ok((cmd is not None or sh is not None) and cmd_ok and sh_ok,
4461
+ f"BB3 the launcher ({where}): the Windows one names the window, runs the panel again when it exits with 86, resolves every label it jumps "
4462
+ "to, and ends by handing the window to a PowerShell prompt that says how to start the panel again - it never falls back to the shell "
4463
+ "underneath, which is how an operator was left at a Python prompt; the shell one does the same with a loop")
4464
+ cmd_ok = sh_ok = True
4465
+ if cmd is not None:
4466
+ lines = cmd.splitlines()
4467
+ cmd_ok = ("auto_restart.flag" in cmd and "GEA_FAILS" in cmd and "GTR 5" in cmd and ":gea_read_flag" in lines
4468
+ and lines[lines.index(":gea_read_flag") + 1].strip().startswith("set /p GEA_AUTO=<") and cmd.count("set /p GEA_AUTO=<") == 1)
4469
+ if sh is not None:
4470
+ sh_ok = "auto_restart.flag" in sh and "fails" in sh and "-le 5" in sh
4471
+ ok(SV.AUTO_MAX == 5 and cmd_ok and sh_ok,
4451
4472
  "BB4 a code that is neither 0 nor 86 is a death, not a decision: the launcher reads the operator's answer from records/auto_restart.flag, "
4452
4473
  "tries at most five times in a row so a panel that crashes while starting hands over the error instead of flickering all night, and reads "
4453
4474
  "that file outside an if-block because a redirect inside parentheses is parsed before the block runs")
4475
+ if cmd is None: # a Linux kit: BB6 needs a Windows launcher text to build its good and bad kits from
4476
+ cmd = ("@echo off\ntitle GEA - Operations Control Panel\n:gea_run\npython\\python.exe -m gea serve\nset GEA_RC=%ERRORLEVEL%\n"
4477
+ "if \"%GEA_RC%\"==\"86\" goto gea_run\n:gea_read_flag\nset /p GEA_AUTO=<\"%GEA_WORKSPACE%\\records\\auto_restart.flag\"\n"
4478
+ "powershell.exe -NoLogo -NoExit -Command \"Write-Host 'start-dashboard.cmd'\"\n")
4454
4479
 
4455
4480
  # -- the flag file: what the launcher will read, not what the manifest says ------------
4456
4481
  ok(SV.auto_restart(wsp) is False and SV.set_auto_restart(wsp, True) is True and SV.auto_restart(wsp) is True
@@ -4461,7 +4486,6 @@ def section_bb_supervisor(tmp: str) -> None:
4461
4486
  "unreadable flag reads as off, since a program does not start itself again on the strength of a file nobody can be sure about")
4462
4487
 
4463
4488
  # -- doctor reads the installed launcher ----------------------------------------------
4464
- from .doctor import check_launcher, check_workspace, kit_dir
4465
4489
  good = d / "kit_good"; (good / "python").mkdir(parents=True)
4466
4490
  (good / "start-dashboard.cmd").write_text(cmd, encoding="utf-8")
4467
4491
  bad = d / "kit_bad"; (bad / "python").mkdir(parents=True)
@@ -4570,9 +4594,139 @@ def section_bb_supervisor(tmp: str) -> None:
4570
4594
  "panel's own page carries the Stop and Restart controls, the authorisation step behind a popup, and the auto-restart switch")
4571
4595
 
4572
4596
 
4597
+ def section_bc_sra(tmp: str) -> None:
4598
+ """Section BC - the Seismicity Response Area packet. The Railroad Commission's operator-led response plans
4599
+ and its Notice to Operators on disposal-well monitoring name a shape; the packet is built to it from what
4600
+ the site holds, and names what it is missing rather than leaving a column blank."""
4601
+ import csv as _csv
4602
+ import subprocess as _sp
4603
+ from datetime import datetime as _dt, timedelta as _td, timezone as _tz
4604
+ from . import sra as SR
4605
+ from .client_reports import forbidden_terms, sra_packet_report, write
4606
+ from .workspace import Workspace, WorkspaceError
4607
+ pkg = Path(__file__).parent
4608
+ d = Path(tmp, "bc"); d.mkdir()
4609
+
4610
+ # -- the self-test: the labelled scene ----------------------------------------------
4611
+ r = SR.selftest()
4612
+ ok(r["status"] == "OK" and all(r["checks"].values()) and r["label"] == "SIMULATION_SELF_TEST"
4613
+ and abs(r["deep_mean_volume_bbl_day"] - 8640.0) / 8640.0 < 0.03 and abs(r["probe_offset_psi"] - 0.465 * 9800) < 15,
4614
+ "BC1 the self-test on a labelled scene: membership by geodesic distance with a NAD27 well's shift named, the four daily parameters from "
4615
+ "the well's own channels with the volume integrated from the rate to within 3 %, two shut-in days at zero, the probe's offset from "
4616
+ "surface pressure recovered, the shallow well refusing a volume it has no rate channel for, the catalogue's trigger and exempt "
4617
+ "aftershock and goal clock, the gaps named, the checkpoints from the plan date")
4618
+
4619
+ # -- the area: declarations, refused when not an area --------------------------------
4620
+ sra = SR.define("Test SRA", 31.95, -102.25, 9.08, "2026-02-25", "NAD27", triggering_event="x")
4621
+ bad = 0
4622
+ for kw in ({"datum": "UNKNOWN"}, {"radius_km": 0}, {"plan_date": "not a date"}, {"name": " "}):
4623
+ try:
4624
+ SR.define(**{**{"name": "T", "lat": 31.95, "lon": -102.25, "radius_km": 9.08, "plan_date": "2026-02-25", "datum": "WGS84"}, **kw});
4625
+ except ValueError:
4626
+ bad += 1
4627
+ cps = SR.checkpoints(sra)
4628
+ ok(sra["centre"]["datum"] == "NAD27" and sra["centre_wgs84"]["shift_m"] > 10 and sra["area_km2"] == 259.0 and sra["threshold_m"] == 3.5
4629
+ and sra["goal_months"] == 18 and sra["response_hours"] == 48 and sra["checkpoint_months"] == 3 and bad == 4
4630
+ and [c["date"] for c in cps][:2] == ["2026-05-25", "2026-08-25"] and cps[-1]["n"] == "goal" and cps[-1]["date"] == "2027-08-25"
4631
+ and "not a measurement" in " ".join(sra["not_a_measurement"]).lower().replace("never derived here", "not a measurement"),
4632
+ "BC2 an area is a centre on a stated datum (carried to WGS84 with its shift named), a radius, a plan date and the plan's numbers - "
4633
+ "M 3.5, 18 months, 48 hours, quarterly - recorded as declarations; an unknown datum, a zero radius, a non-date or a blank name is refused; "
4634
+ "the checkpoints fall every three months from the plan date and the goal date closes the list")
4635
+
4636
+ # -- the catalogue reader: a TexNet-shaped export ------------------------------------
4637
+ cat = d / "texnet.csv"
4638
+ with open(cat, "w", newline="") as f:
4639
+ w = _csv.writer(f); w.writerow(["EventID", "Origin Date", "Origin Time", "Local Magnitude", "Latitude", "Longitude", "Depth of Hypocenter (Km)"])
4640
+ w.writerow(["texnet2026aaaa", "2026-02-20", "04:15:10", "4.4", "31.950", "-102.250", "7.2"]) # before the plan date
4641
+ w.writerow(["texnet2026aaab", "2026-03-05", "11:02:33", "3.7", "31.960", "-102.240", "6.8"]) # counts
4642
+ w.writerow(["texnet2026aaac", "2026-03-06", "01:00:00", "3.6", "31.955", "-102.245", "6.5"]) # declared aftershock
4643
+ w.writerow(["texnet2026aaad", "2026-03-09", "21:40:00", "2.0", "31.940", "-102.260", "6.1"])
4644
+ w.writerow(["texnet2026aaae", "2026-03-12", "03:00:00", "3.9", "32.500", "-102.700", "8.0"]) # outside
4645
+ w.writerow(["", "", "", "", "", "", ""]) # a blank line is skipped, not an event
4646
+ c = SR.read_catalog(str(cat))
4647
+ se = SR.seismicity(sra, c, as_of=_dt(2026, 4, 1, tzinfo=_tz.utc), aftershocks=["texnet2026aaac"])
4648
+ ok(c["n_events"] == 5 and c["n_skipped"] == 1 and c["columns"]["origin_date"] == "Origin Date" and c["columns"]["origin_time"] == "Origin Time"
4649
+ and c["events"][0]["time"] == "2026-02-20T04:15:10Z" and c["events"][0]["depth_km"] == 7.2
4650
+ and se["n_inside"] == 4 and se["n_at_or_above"] == 3 and se["n_counted"] == 1 and se["n_exempt"] == 1
4651
+ and se["largest"]["event_id"] == "texnet2026aaaa" and se["trigger"]["status"] == "TRIGGERED" and se["trigger"]["event_id"] == "texnet2026aaab"
4652
+ and se["trigger"]["respond_by"] == "2026-03-07T11:02:33Z" and se["goal"]["clock_started"] == "2026-03-05" and se["goal"]["status"] == "RUNNING"
4653
+ and se["goal"]["goal_date"] == "2027-09-05",
4654
+ "BC3 a TexNet-shaped export is read by its column names (date and time in separate columns, the depth under its long name), a blank line "
4655
+ "is skipped and counted; against the plan: the M 4.4 before the plan date is the largest but does not count, the declared aftershock is "
4656
+ "exempt, the M 3.9 outside the circle is outside, the M 3.7 triggers the 48-hour response with its deadline, and the 18-month clock "
4657
+ "starts from it")
4658
+
4659
+ # -- the daily record from a stream: refused without a calendar --------------------------------
4660
+ from .ports import LiveStream, StreamChannel
4661
+ import numpy as _np
4662
+ st = LiveStream(name="x", source_format="synthetic", index_kind="time_s", index=_np.arange(100) * 600.0,
4663
+ channels={"P": StreamChannel("P", "psi", _np.full(100, 1500.0))}, meta={})
4664
+ rr = SR.daily_records(st, {"channel_pressure": "P"})
4665
+ sc = SR.synthetic_scene(days=3)
4666
+ dr = SR.daily_records(sc["streams"]["deep-1"], {**sc["wells"][0]["disposal"], "rate_unit": "furlongs"})
4667
+ ok(rr["status"] == "REFUSED" and "calendar" in rr["detail"]
4668
+ and dr["parameters"]["injection_volume_bbl"]["status"] == "NOT RECORDED" and "furlongs" in dr["parameters"]["injection_volume_bbl"]["why"]
4669
+ and dr["parameters"]["max_surface_injection_pressure_psi"]["status"] == "RECORDED",
4670
+ "BC4 a daily record needs a calendar: a stream with no start time is refused, not dated from zero; a rate in a unit the program does not "
4671
+ "convert leaves the volume NOT RECORDED with the unit named, while the pressures are still recorded")
4672
+
4673
+ # -- through the workspace and the command line ----------------------------------------
4674
+ t0 = _dt(2026, 3, 1, tzinfo=_tz.utc); n = 6 * 144
4675
+ hist = d / "swd.csv"
4676
+ with open(hist, "w", newline="") as f:
4677
+ w = _csv.writer(f); w.writerow(["timestamp", "P_surf_psi", "Q_bpm", "P_bh_psi"])
4678
+ for i in range(n):
4679
+ w.writerow([(t0 + _td(seconds=600 * i)).strftime("%Y-%m-%dT%H:%M:%SZ"), "1500.0", "5.0", f"{1500.0 + 0.465 * 9000:.1f}"])
4680
+ ws = Workspace.create(str(d / "ws"), "Pad 3", actor="tester")
4681
+ w1 = ws.add_well_file(str(hist), display="SWD 1", actor="tester")
4682
+ try:
4683
+ ws.set_well(w1["id"], disposal={"depth_tier": "middle"}, actor="tester"); tier_bad = False
4684
+ except WorkspaceError:
4685
+ tier_bad = True
4686
+ run = lambda *a: _sp.run([sys.executable, "-m", "gea", "workspace", "--path", ws.path, "--actor", "tester", *a], capture_output=True, text=True)
4687
+ r1 = run("--action", "set-well", "--well", w1["id"], "--lat", "31.96", "--lon", "-102.24", "--datum", "NAD27", "--api", "42-329-12345", "--uic", "123456",
4688
+ "--depth-tier", "deep", "--formation", "completed below the base of the Wolfcamp", "--channel-pressure", "P_surf_psi", "--channel-rate", "Q_bpm",
4689
+ "--rate-unit", "bbl/min", "--bhp-method", "probe", "--channel-bhp", "P_bh_psi")
4690
+ r2 = run("--action", "add-site", "--name", "SRA block", "--wells", w1["id"], "--client", "Acme Operating")
4691
+ r3 = run("--action", "sra-report", "--site", "SRA-block") # no area yet: refused
4692
+ r4 = run("--action", "sra-define", "--site", "SRA-block", "--name", "Test SRA", "--lat", "31.95", "--lon", "-102.25", "--datum", "WGS84",
4693
+ "--radius-km", "9.08", "--plan-date", "2026-02-25")
4694
+ r5 = run("--action", "sra-report", "--site", "SRA-block", "--catalog", str(cat), "--aftershocks", "texnet2026aaac")
4695
+ ws2 = Workspace(ws.path)
4696
+ out = Path(ws.path, "reports", "sites", "SRA-block")
4697
+ rows = list(_csv.DictReader(open(out / "sra_daily_export.csv", encoding="utf-8")))
4698
+ md = (out / "sra_packet.md").read_text(encoding="utf-8")
4699
+ audit = Path(ws.path, "records", "audit.jsonl").read_text(encoding="utf-8")
4700
+ pkj = json.loads((out / "sra_packet.json").read_text(encoding="utf-8"))["packet"]
4701
+ ok(tier_bad and r1.returncode == 0 and r2.returncode == 0 and r3.returncode != 0 and "sra-define" in (r3.stderr + r3.stdout)
4702
+ and r4.returncode == 0 and r5.returncode == 0 and "COMPLETE" in r5.stdout and "TRIGGERED" in r5.stdout
4703
+ and ws2.site("SRA-block")["sra"]["name"] == "Test SRA" and pkj["status"] == "COMPLETE" and pkj["wells"][0]["inside"] and pkj["wells"][0]["depth_tier"] == "deep"
4704
+ and len(rows) == 6 and rows[0]["API number"] == "42-329-12345" and rows[0]["UIC permit number"] == "123456"
4705
+ and abs(float(rows[1]["Injection volume (barrels per day)"]) - 7200.0) < 10 and rows[1]["Maximum surface injection pressure (pounds per square inch)"] == "1500.0"
4706
+ and abs(float(rows[1]["Bottomhole pressure (pounds per square inch)"]) - (1500 + 0.465 * 9000)) < 1 and rows[1]["BHP method"] == "probe"
4707
+ and "Maximum surface injection pressure (pounds per square inch)" in rows[0]
4708
+ and not forbidden_terms(md) and "well.declare" in audit and "site.sra_define" in audit and "site.sra_packet" in audit,
4709
+ "BC5 `set-well` records what the operator declares - surface position and datum, API and UIC, depth tier by the named formation, which "
4710
+ "channels are the surface pressure, the rate and the downhole gauge, the BHP method - and refuses a tier that is not shallow or deep; "
4711
+ "`sra-define` puts the area on the site; `sra-report` is refused until it does; then the packet is COMPLETE for a well with everything "
4712
+ "declared, the daily export carries the Notice's parameter names with the API and UIC on every line, the volume integrates to 7,200 bbl "
4713
+ "a day from 5 bbl/min, the probe is the bottomhole pressure, the report passes the vocabulary gate, and every step is audited")
4714
+
4715
+ # -- the service offers it and the page carries it ------------------------------------------
4716
+ svc_src = (pkg / "service.py").read_text(encoding="utf-8"); page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
4717
+ r6 = _sp.run([sys.executable, "-m", "gea", "sra"], capture_output=True, text=True)
4718
+ ok("'sra_report'" in svc_src and "sra_packet.html" in svc_src and "Seismicity Response Area</th>" in page_src and "sra-define" in page_src
4719
+ and r6.returncode == 0 and "SIMULATION_SELF_TEST" in r6.stdout and "OK" in r6.stdout
4720
+ and (pkg / "help" / "sra.md").is_file(),
4721
+ "BC6 the Sites page carries the area and the packet's status beside each site; `gea sra` runs the self-test from the command line and "
4722
+ "labels it SIMULATION_SELF_TEST; the help page exists")
4723
+
4724
+
4573
4725
  def main() -> int:
4574
4726
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
4575
- with tempfile.TemporaryDirectory() as tmp:
4727
+ # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
4728
+ # the checks have already passed or failed on their own, and the folder is the temp folder
4729
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
4576
4730
  section_a_cli(tmp)
4577
4731
  section_b_las(tmp)
4578
4732
  section_c_reconciler(tmp)
@@ -4625,6 +4779,7 @@ def main() -> int:
4625
4779
  section_az_uncertainty(tmp)
4626
4780
  section_ba_sites(tmp)
4627
4781
  section_bb_supervisor(tmp)
4782
+ section_bc_sra(tmp)
4628
4783
  if _FAILS:
4629
4784
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
4630
4785
  for f in _FAILS:
@@ -1584,6 +1584,84 @@ def seismic_track_report(track: dict, histories: List[dict], positions: dict, ve
1584
1584
  'evaluated_at_utc': now.strftime('%Y-%m-%dT%H:%M:%SZ'), '_records': [], '_catalogue_obj': None})
1585
1585
 
1586
1586
 
1587
+ def sra_packet_report(pk: dict, evaluated_at: Optional[datetime] = None, program_version: str = '') -> Document:
1588
+ """The Seismicity Response Area packet: what an operator inside an SRA puts in front of the Railroad
1589
+ Commission, built from what the site holds. The area and the plan as declared; which wells and stations
1590
+ are inside it and each well's depth tier; the four daily parameters the Commission's Notice names, per
1591
+ well, rolled up by month, with the bottomhole-pressure method; the catalogue's events against the plan -
1592
+ the response trigger and the goal clock; the checkpoint schedule; and every gap by name."""
1593
+ now = evaluated_at or _utc_now()
1594
+ s = pk['sra']; site = pk.get('site') or {}; m = pk['membership']; se = pk.get('seismicity')
1595
+ report_id = f"SRA-{str(site.get('id') or s['name']).replace('/', '-')[:24]}-{now.strftime('%Y%m%dT%H%M%SZ')}"
1596
+ secs = []
1597
+ secs.append(Section('1', 'The area and the plan', [
1598
+ f"{s['name']}: a circle of {s['radius_km']:g} km ({s['area_km2']:,} km\u00b2) around {s['centre']['lat']:.5f}, {s['centre']['lon']:.5f} "
1599
+ f"on {s['centre']['datum']}. Plan date {s['plan_date']}. The plan is written against earthquakes of magnitude {s['threshold_m']:.1f} and "
1600
+ f"above inside the area: the goal is {s['goal_months']} months without one; the response to one is a meeting of the operator-led "
1601
+ f"response group within {s['response_hours']} hours; the checkpoints with Commission staff fall every {s['checkpoint_months']} months. "
1602
+ f"Disposal wells are placed in two tiers by the {s['formation_boundary']}."
1603
+ + (f" Triggering event: {s['triggering_event']}." if s.get('triggering_event') else '')
1604
+ + (f" {s['note']}" if s.get('note') else ''),
1605
+ 'Every number in this paragraph is a declaration copied from the plan, not a measurement.']))
1606
+ wrows = [[w['id'], w['display'] or '-', w['api_number'] or '-', w['uic_number'] or '-',
1607
+ ('inside' if w['inside'] else ('outside' if w['inside'] is False else 'undecided')),
1608
+ _fmt(w['distance_km']), w['depth_tier'], (w['bhp_method'] or '-'), w.get('daily_status') or '-'] for w in pk['wells']]
1609
+ srows = [[x['id'], x.get('display') or '-', x.get('kind') or '-', ('inside' if x['inside'] else ('outside' if x['inside'] is False else 'undecided')),
1610
+ _fmt(x.get('distance_km'))] for x in pk['stations']]
1611
+ secs.append(Section('2', 'What is inside the area', [
1612
+ f"{m['n_wells_inside']} well(s) and {m['n_stations_inside']} seismic station(s) of the site fall inside the area by geodesic distance "
1613
+ f"from the centre, with every position carried to WGS84 first; {m['n_undecided']} could not be placed."],
1614
+ [Table(['Well', 'Name', 'API', 'UIC', 'Membership', 'Distance (km)', 'Depth tier', 'BHP method', 'Daily record'], wrows, 'Wells'),
1615
+ Table(['Station', 'Name', 'Kind', 'Membership', 'Distance (km)'], srows, 'Seismic stations')]))
1616
+ n = 3
1617
+ inside_wells = [w for w in pk['wells'] if w['inside'] and w.get('months')]
1618
+ for w in inside_wells:
1619
+ pr = w.get('parameters', {}); bh = w.get('bhp', {})
1620
+ paras = [f"{w['display'] or w['id']} ({w['depth_tier']}): {w.get('daily_detail') or ''}",
1621
+ ' '.join(f"{v['label']}: {v['status']}" + (f" from channel {v['channel']}" if v.get('channel') else '') + '.' for v in pr.values()),
1622
+ f"Bottomhole pressure: {bh.get('label', '-')}, reported {bh.get('reported', '-')}" + (f"; {bh['how']}" if bh.get('how') else '') + '.']
1623
+ secs.append(Section(str(n), f"Daily record, rolled up by month: {w['display'] or w['id']}", paras,
1624
+ [Table(['Month', 'Days', 'Recorded', 'Missing', 'Max surface inj. pressure (psi)', 'Avg surface inj. pressure (psi)',
1625
+ 'Injection volume (bbl)', 'Max injection rate (bbl/min)', 'BHP (psi)'],
1626
+ [[r['month'], r['days'], r['recorded'], r['missing'], _fmt(r['max_surface_injection_pressure_psi']),
1627
+ _fmt(r['avg_surface_injection_pressure_psi']), _fmt(r['injection_volume_bbl']),
1628
+ _fmt(r['max_injection_rate_bbl_min']), _fmt(r['bhp_psi'])] for r in w['months']])]))
1629
+ n += 1
1630
+ if se:
1631
+ lg = se.get('largest')
1632
+ paras = [f"Catalogue export of {se['catalog'].get('export_mtime_utc', '-')[:10]}: {se['n_inside']} event(s) inside the area, "
1633
+ f"{se['n_at_or_above']} at or above M {s['threshold_m']:.1f}, {se['n_exempt']} of those declared aftershocks and exempt."
1634
+ + (f" Largest inside the area: M {lg['magnitude']:.1f}, {lg['time'][:10]}, {lg['distance_km']:g} km from the centre ({lg['event_id']})." if lg else ''),
1635
+ f"Response: {se['trigger']['status']}. {se['trigger']['detail']}.",
1636
+ f"Goal: {se['goal']['status']}. {se['goal']['detail']}."]
1637
+ ev = [[e['event_id'], e['time'][:16].replace('T', ' '), f"{e['magnitude']:.1f}", _fmt(e['distance_km']), _fmt(e.get('depth_km')),
1638
+ ('yes' if e['after_plan'] else 'no'), ('exempt' if e['exempt'] else ('counts' if (e['at_or_above'] and e['after_plan']) else '-'))]
1639
+ for e in se['events'] if e['at_or_above'] or e is se['events'][-1]]
1640
+ secs.append(Section(str(n), 'Seismicity against the plan', paras,
1641
+ [Table(['Event', 'Origin (UTC)', 'M', 'Distance (km)', 'Depth (km)', 'After plan date', 'Standing'], ev,
1642
+ 'Events at or above the threshold inside the area (and the latest event)')]))
1643
+ n += 1
1644
+ else:
1645
+ secs.append(Section(str(n), 'Seismicity against the plan', ['No catalogue export was handed in. This section is empty and the goal clock is not stated.']))
1646
+ n += 1
1647
+ secs.append(Section(str(n), 'The schedule', ['Checkpoints with Commission staff from the plan date, and the goal date.'],
1648
+ [Table(['#', 'Date', 'What'], [[c['n'], c['date'], c['what']] for c in pk['checkpoints']])]))
1649
+ n += 1
1650
+ secs.append(Section(str(n), 'What this packet is missing', pk['gaps'] or ['Nothing: every declaration is present and every parameter is recorded.']))
1651
+ secs.append(Section(str(n + 1), 'What this packet does not call a measurement',
1652
+ [x.rstrip('.') + '.' for x in dict.fromkeys(pk.get('not_a_measurement') or [])]))
1653
+ front = [['Report ID', report_id], ['Area', s['name']], ['Site', site.get('display') or '-'], ['Client', site.get('client') or '-'],
1654
+ ['Generated (UTC)', now.strftime('%Y-%m-%dT%H:%M:%SZ')],
1655
+ ['Program', f'{PROGRAM_NAME}' + (f' build {program_version}' if program_version else '')],
1656
+ ['Result', f"{pk['status']}: {m['n_wells_inside']} WELL(S) INSIDE, " + (f"RESPONSE {se['trigger']['status']}, GOAL {se['goal']['status']}" if se else 'NO CATALOGUE')
1657
+ + (f", {len(pk['gaps'])} GAP(S)" if pk['gaps'] else '')]]
1658
+ return Document(title=f'{PROGRAM_NAME} - Seismicity Response Area Packet', report_id=report_id, front=front, sections=secs,
1659
+ footer=('Built to the operator-led response plans the Railroad Commission of Texas has on file and its December 2023 Notice to '
1660
+ 'Operators on disposal-well monitoring in the Permian Basin. The daily export carries the Notice\'s parameter names; '
1661
+ 'the mapping onto the TexNet reporting tool\'s template is the operator\'s step.'),
1662
+ data={'packet': pk, 'report_id': report_id, 'evaluated_at_utc': now.strftime('%Y-%m-%dT%H:%M:%SZ'), '_records': [], '_catalogue_obj': None})
1663
+
1664
+
1587
1665
  def forbidden_terms(text: str) -> List[str]:
1588
1666
  low = text.lower()
1589
1667
  return [t for t in FORBIDDEN_TERMS if t in low]
@@ -0,0 +1,15 @@
1
+ # The Seismicity Response Area
2
+
3
+ > What an operator inside a Railroad Commission Seismicity Response Area puts in front of the Commission, built from what the site holds.
4
+
5
+ **The command**: declare each disposal well once - `gea workspace --path C:\site --action set-well --well <id> --lat LAT --lon LON --datum NAD27 --api 42-329-12345 --uic 123456 --depth-tier deep --formation "completed below the base of the Wolfcamp" --channel-pressure P_surf_psi --channel-rate Q_bpm --rate-unit bbl/min --bhp-method probe --channel-bhp P_bh_psi`; define the area on the site once - `--action sra-define --site <id> --name "Gardendale SRA" --lat 31.95 --lon -102.25 --datum WGS84 --radius-km 9.08 --plan-date 2022-12-16 --triggering-event texnet2022yxab`; then every month - `--action sra-report --site <id> --catalog texnet_export.csv [--aftershocks id1 id2] [--start 2026-03-01 --end 2026-03-31]`. `gea sra` runs the self-test on a labelled scene.
6
+
7
+ **What it writes**: `reports/sites/<site>/sra_packet.html` (and `.md`, `.json`) - the area and the plan as declared, what is inside it with each well's depth tier, the daily record per well rolled up by month, the seismicity against the plan, the checkpoint schedule, and every gap by name; and `sra_daily_export.csv` - one line per well per day under the Notice's own parameter names (maximum surface injection pressure, average surface injection pressure, injection volume, maximum injection rate), the bottomhole pressure and its method, and the well's API and UIC numbers. Every declaration and every packet is in `records/audit.jsonl`.
8
+
9
+ **The number to check**: `Response:` in section "Seismicity against the plan" - TRIGGERED names the event, its magnitude and the deadline for the response group's meeting; NOT TRIGGERED names why. `Goal:` gives the days without a threshold event and the date the plan's goal falls on. The packet's front says COMPLETE or INCOMPLETE with the count of gaps.
10
+
11
+ **What this page will not call a measurement**: which earthquakes are aftershocks - the operator declares them with `--aftershocks` and they are shown as exempt, never decided here. A volume read off a pressure gauge - a well with no rate channel has no volume and no maximum rate, and the packet says NOT RECORDED. A depth tier from depth alone - the tier is stratigraphic, above or below the named formation base, and is declared. The TexNet reporting tool's own upload template - the export carries the Notice's parameter names and the mapping is the operator's step. A catalogue nobody handed in - the catalogue is a file the operator exports from TexNet, and its export date is on the packet.
12
+
13
+ The shape is the Commission's: the operator-led response plans on file (Gardendale, Northern Culberson-Reeves, Stanton) - a circle of about 9 km, a plan written against M 3.5, an 18-month goal, a response group meeting within 48 hours of a threshold event, quarterly checkpoints with Commission staff, and disposal wells in two tiers by the base of the Wolfcamp - and the December 2023 Notice to Operators on disposal-well monitoring and reporting in the Permian Basin, which names the four daily parameters and the three bottomhole-pressure methods. A downhole gauge this program monitors is the Notice's permanent probe.
14
+
15
+ Related: `gea help sites`, `gea help seismic`, `gea help data-in`.
@@ -38,6 +38,7 @@ INDEX: List[tuple] = [
38
38
  ('patches', 'Patches', 'supervised live connections, their states and records'),
39
39
  ('files', 'Files', 'import and export roots, detection by content, the evidence pack'),
40
40
  ('notifications', 'Notifications', 'rules, channels, the delivery log'),
41
+ ('sra', 'The Seismicity Response Area', 'the packet an operator inside an SRA puts in front of the Commission'),
41
42
  ('doctor', 'Which code is running', 'gea doctor and the serve start-up gate'),
42
43
  ('audit-update', 'Audit / Update', 'the whole audit log with filters, the program against PyPI, every report against its source'),
43
44
  ('upkeep', 'Upkeep', 'housekeeping, sessions, the TLS proxy, the load test'),
@@ -62,17 +62,24 @@ class JobRunner:
62
62
  def _dir(self, job_id: str) -> str:
63
63
  return os.path.join(self.ws.jobs_dir, job_id)
64
64
 
65
- def _read(self, job_id: str) -> dict:
66
- p = os.path.join(self._dir(job_id), 'job.json')
67
- for attempt in range(40): # a writer may be mid-replace; the file is never half-written (see _write)
65
+ @staticmethod
66
+ def _read_json(p: str, attempts: int = 40) -> dict:
67
+ """Read a job file that a writer may be mid-replace on. The file is never half-written (see _write), but
68
+ on Windows a read during os.replace is refused with PermissionError, and the v0.7.0 kit run #8 and the
69
+ v0.9.0 ship both fell over on exactly that - the first in _read, the second in list(), which had its own
70
+ open() and none of this. Every read of a job file goes through here now."""
71
+ for attempt in range(attempts):
68
72
  try:
69
73
  with open(p, encoding='utf-8') as f:
70
74
  return json.load(f)
71
75
  except (json.JSONDecodeError, FileNotFoundError, PermissionError):
72
- time.sleep(0.05) # PermissionError: Windows refuses a read during os.replace (the v0.7.0 kit run #8)
76
+ time.sleep(0.05)
73
77
  with open(p, encoding='utf-8') as f:
74
78
  return json.load(f)
75
79
 
80
+ def _read(self, job_id: str) -> dict:
81
+ return self._read_json(os.path.join(self._dir(job_id), 'job.json'))
82
+
76
83
  def _write(self, job: dict) -> None:
77
84
  p = os.path.join(self._dir(job['id']), 'job.json')
78
85
  tmp = p + '.tmp'
@@ -119,8 +126,10 @@ class JobRunner:
119
126
  for name in sorted(os.listdir(self.ws.jobs_dir), reverse=True):
120
127
  p = os.path.join(self.ws.jobs_dir, name, 'job.json')
121
128
  if os.path.isfile(p):
122
- with open(p, encoding='utf-8') as f:
123
- j = json.load(f)
129
+ try:
130
+ j = self._read_json(p)
131
+ except (OSError, json.JSONDecodeError):
132
+ continue # a job whose file cannot be read right now is left out of this listing, not a crash
124
133
  if status is None or j['status'] == status:
125
134
  out.append(j)
126
135
  if len(out) >= limit: