1- import json
21import os
32import sys
43import threading
76
87from geophires_x import GEOPHIRESv3 as geophires
98
9+ # Assuming these are in a sibling file or accessible path
1010from .common import _get_logger
11- from .geophires_input_parameters import EndUseOption
1211from .geophires_input_parameters import GeophiresInputParameters
12+ from .geophires_input_parameters import ImmutableGeophiresInputParameters
1313from .geophires_x_result import GeophiresXResult
1414
1515
1616class GeophiresXClient :
17+ """
18+ A thread-safe and process-safe client for running GEOPHIRES simulations.
19+ Relies on an explicit shutdown() call to clean up background processes.
20+ """
21+
1722 # --- Class-level shared resources ---
18- # These will be initialized lazily and shared across all instances and processes.
1923 _manager = None
2024 _cache = None
21- _lock = None # This will be a process-safe RLock from the manager.
25+ _lock = None
2226
2327 # A standard threading lock to make the one-time initialization thread-safe.
2428 _init_lock = threading .Lock ()
@@ -31,7 +35,6 @@ def __init__(self, enable_caching=True, logger_name=None):
3135 self ._enable_caching = enable_caching
3236
3337 # Lazy-initialize shared resources if they haven't been already.
34- # This approach is safe to call from multiple threads/processes.
3538 if GeophiresXClient ._manager is None :
3639 self ._initialize_shared_resources ()
3740
@@ -41,69 +44,63 @@ def _initialize_shared_resources(cls):
4144 Initializes the multiprocessing Manager and shared resources (cache, lock)
4245 in a thread-safe and process-safe manner.
4346 """
44- # Use a thread-safe lock to ensure this block only ever runs once
45- # across all threads in the main process.
4647 with cls ._init_lock :
47- # The double-check locking pattern ensures we don't try to
48- # re-initialize if another thread finished while we were waiting.
4948 if cls ._manager is None :
5049 cls ._manager = Manager ()
5150 cls ._cache = cls ._manager .dict ()
52- cls ._lock = cls ._manager .RLock () # The Manager now creates the lock.
51+ cls ._lock = cls ._manager .RLock ()
5352
54- def get_geophires_result (self , input_params : GeophiresInputParameters ) -> GeophiresXResult :
53+ @classmethod
54+ def shutdown (cls ):
5555 """
56- Calculates a GEOPHIRES result in a thread-safe and process-safe manner.
56+ Explicitly shuts down the background manager process.
57+ This MUST be called when the application is finished with the client
58+ to prevent orphaned processes.
5759 """
58- # Use the process-safe lock from the manager to make the check-then-act
59- # operation on the cache fully atomic across multiple processes.
60- with GeophiresXClient . _lock :
61- cache_key = hash ( input_params )
62- if self . _enable_caching and cache_key in GeophiresXClient ._cache :
63- return GeophiresXClient . _cache [ cache_key ]
60+ with cls . _init_lock :
61+ if cls . _manager is not None :
62+ cls . _manager . shutdown ()
63+ cls . _manager = None
64+ cls ._cache = None
65+ cls . _lock = None
6466
65- # --- This section is now guaranteed to run only once per unique input ---
66- stash_cwd = Path .cwd ()
67- stash_sys_argv = sys .argv
67+ def get_geophires_result (self , input_params : GeophiresInputParameters ) -> GeophiresXResult :
68+ """
69+ Calculates a GEOPHIRES result, using a cross-process cache to avoid
70+ re-computing results for the same inputs. Caching is only effective
71+ when providing an instance of ImmutableGeophiresInputParameters.
72+ """
73+ is_immutable = isinstance (input_params , ImmutableGeophiresInputParameters )
6874
69- sys .argv = ['' , input_params .as_file_path (), input_params .get_output_file_path ()]
70- try :
71- geophires .main (enable_geophires_logging_config = False )
72- except Exception as e :
73- raise RuntimeError (f'GEOPHIRES encountered an exception: { e !s} ' ) from e
74- except SystemExit :
75- raise RuntimeError ('GEOPHIRES exited without giving a reason' ) from None
76- finally :
77- # Ensure global state is restored even if geophires.main() fails
78- sys .argv = stash_sys_argv
79- os .chdir (stash_cwd )
75+ if not (self ._enable_caching and is_immutable ):
76+ return self ._run_simulation (input_params )
8077
81- self . _logger . info ( f'GEOPHIRES-X output file: { input_params . get_output_file_path () } ' )
78+ cache_key = hash ( input_params )
8279
83- result = GeophiresXResult ( input_params . get_output_file_path ())
84- if self . _enable_caching :
85- self ._cache [cache_key ] = result
80+ with GeophiresXClient . _lock :
81+ if cache_key in GeophiresXClient . _cache :
82+ return GeophiresXClient ._cache [cache_key ]
8683
84+ result = self ._run_simulation (input_params )
85+ GeophiresXClient ._cache [cache_key ] = result
8786 return result
8887
89-
90- if __name__ == '__main__' :
91- # This block remains for direct testing of the script.
92- client = GeophiresXClient ()
93- log = _get_logger ()
94-
95- params = GeophiresInputParameters (
96- {
97- 'Print Output to Console' : 0 ,
98- 'End-Use Option' : EndUseOption .DIRECT_USE_HEAT .value ,
99- 'Reservoir Model' : 1 ,
100- 'Time steps per year' : 1 ,
101- 'Reservoir Depth' : 3 ,
102- 'Gradient 1' : 50 ,
103- 'Maximum Temperature' : 250 ,
104- }
105- )
106-
107- result_ = client .get_geophires_result (params )
108- log .info (f'Breakeven price: ${ result_ .direct_use_heat_breakeven_price_USD_per_MMBTU } /MMBTU' )
109- log .info (json .dumps (result_ .result , indent = 2 ))
88+ def _run_simulation (self , input_params : GeophiresInputParameters ) -> GeophiresXResult :
89+ """Helper method to encapsulate the actual GEOPHIRES run."""
90+ stash_cwd = Path .cwd ()
91+ stash_sys_argv = sys .argv
92+ sys .argv = ['' , input_params .as_file_path (), input_params .get_output_file_path ()]
93+
94+ try :
95+ geophires .main (enable_geophires_logging_config = False )
96+ except Exception as e :
97+ raise RuntimeError (f'GEOPHIRES encountered an exception: { e !s} ' ) from e
98+ except SystemExit :
99+ raise RuntimeError ('GEOPHIRES exited without giving a reason' ) from None
100+ finally :
101+ sys .argv = stash_sys_argv
102+ os .chdir (stash_cwd )
103+
104+ self ._logger .info (f'GEOPHIRES-X output file: { input_params .get_output_file_path ()} ' )
105+ result = GeophiresXResult (input_params .get_output_file_path ())
106+ return result
0 commit comments