!> @file solver_session.f90 !> @brief Polling-friendly session API built on top of `solver_runtime`. module solver_session use precision, only: wp use config, only: config_t, read_config, validate_config use config_schema, only: config_get_integer, config_get_real, config_get_logical, & config_get_string, config_get_real3, config_set_integer, & config_set_real, config_set_logical, config_set_string, & config_set_real3 use solver_runtime, only: solver_run_context_t, initialize_runtime, run_solver, & run_solver_steps, finalize_runtime, report_performance_summary, teardown_runtime, & copy_current_solution, write_solution_file use checkpoint, only: write_checkpoint implicit none private !> Operation completed successfully. integer, parameter, public :: solver_status_ok = 0 !> Caller supplied an invalid handle, key, buffer, or scalar argument. integer, parameter, public :: solver_status_invalid_argument = 1 !> Operation requires an initialised runtime, but the session is not ready. integer, parameter, public :: solver_status_invalid_state = 2 !> Config loading or validation failed. integer, parameter, public :: solver_status_config_error = 3 !> File I/O or other runtime setup/output work failed. integer, parameter, public :: solver_status_io_error = 4 !> Reserved for adapters that need to report temporary v1 exclusivity/busy state. integer, parameter, public :: solver_status_busy = 5 !> Polling-friendly runtime summary exposed to adapters. !! !! All fields are copy-out values. No pointer aliasing or ownership is shared !! with the underlying runtime state. type, public :: solver_progress_t integer :: iteration = 0 !< Completed step count. integer :: n_point = 0 !< Grid-point count (`n_cell + 1`). logical :: is_initialized = .false. !< True after `solver_session_initialize`. logical :: is_finished = .false. !< True once `time_stop` has been reached. real(wp) :: sim_time = 0.0_wp !< Current simulation time. [s] real(wp) :: time_stop = 0.0_wp !< Configured final simulation time. [s] real(wp) :: dt = 0.0_wp !< Most recent or configured time step. [s] real(wp) :: residual = 0.0_wp !< Global residual scalar reported by the runtime. end type solver_progress_t !> Mutable solver session that owns one configuration and, optionally, one runtime. !! !! V1 keeps exactly one runtime context inside a session. Any setter or new !! namelist load invalidates the active runtime so the next initialise call !! rebuilds state from a coherent configuration snapshot. type, public :: solver_session_t type(config_t) :: cfg !< Authoritative editable configuration. type(solver_run_context_t) :: ctx !< Runtime context once initialised. logical :: has_runtime = .false. !< True when `ctx` owns live allocations. character(len=512) :: last_error = '' !< Last surfaced writable-operation error. end type solver_session_t public :: solver_session_create, solver_session_destroy public :: solver_session_load_namelist, solver_session_initialize public :: solver_session_advance, solver_session_run_to_end public :: solver_session_get_progress, solver_session_get_point_count public :: solver_session_copy_solution, solver_session_write_result public :: solver_session_write_checkpoint, solver_session_last_error public :: solver_session_get_integer, solver_session_get_real, solver_session_get_logical public :: solver_session_get_string, solver_session_get_real3 public :: solver_session_set_integer, solver_session_set_real, solver_session_set_logical public :: solver_session_set_string, solver_session_set_real3 contains !> Reset a session to its default empty state. subroutine solver_session_create(session) type(solver_session_t), intent(out) :: session session = solver_session_t() end subroutine solver_session_create !> Tear down any live runtime allocations and clear the last error. subroutine solver_session_destroy(session) type(solver_session_t), intent(inout) :: session call reset_runtime(session) session % last_error = '' end subroutine solver_session_destroy !> Load configuration from a namelist file into the session. !! !! Any currently initialised runtime is discarded first so the session cannot !! retain stale arrays that disagree with the newly loaded configuration. subroutine solver_session_load_namelist(session, filename, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in) :: filename integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err call reset_runtime(session) call read_config(filename, session % cfg, ok, err) if (.not. ok) then call finish(session, solver_status_config_error, err, status, message) return end if call finish(session, solver_status_ok, '', status, message) end subroutine solver_session_load_namelist !> Validate the current configuration and allocate the runtime context. subroutine solver_session_initialize(session, status, message) type(solver_session_t), intent(inout) :: session integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err call reset_runtime(session) call validate_config(session % cfg, ok, err) if (.not. ok) then call finish(session, solver_status_config_error, err, status, message) return end if call initialize_runtime(session % ctx, session % cfg, 'session', ok, err) if (.not. ok) then call teardown_runtime(session % ctx) call finish(session, solver_status_io_error, err, status, message) return end if session % has_runtime = .true. call finish(session, solver_status_ok, '', status, message) end subroutine solver_session_initialize !> Advance the runtime by at most `max_steps` iterations. !! !! This is the canonical polling-first stepping API used by GUI-facing !! adapters. Checkpoints and compatibility snapshot-file writes still occur !! inside the bounded loop so polling and CLI code paths stay numerically aligned. subroutine solver_session_advance(session, max_steps, steps_taken, finished, status, message) type(solver_session_t), intent(inout) :: session integer, intent(in) :: max_steps integer, intent(out) :: steps_taken logical, intent(out) :: finished integer, intent(out), optional :: status character(len=*), intent(out), optional :: message steps_taken = 0 finished = .false. if (.not. session % has_runtime) then call finish(session, solver_status_invalid_state, 'solver_session: session is not initialised', status, message) return end if if (max_steps < 0) then call finish(session, solver_status_invalid_argument, 'solver_session: max_steps must be >= 0', status, message) return end if call run_solver_steps(session % ctx, max_steps, steps_taken, finished) call finish(session, solver_status_ok, '', status, message) end subroutine solver_session_advance !> Run the current session until `time_stop` is reached. subroutine solver_session_run_to_end(session, status, message) type(solver_session_t), intent(inout) :: session integer, intent(out), optional :: status character(len=*), intent(out), optional :: message if (.not. session % has_runtime) then call finish(session, solver_status_invalid_state, 'solver_session: session is not initialised', status, message) return end if call run_solver(session % ctx) call finish(session, solver_status_ok, '', status, message) end subroutine solver_session_run_to_end !> Copy out a lightweight progress snapshot for polling clients. subroutine solver_session_get_progress(session, progress, status, message) type(solver_session_t), intent(in) :: session type(solver_progress_t), intent(out) :: progress integer, intent(out), optional :: status character(len=*), intent(out), optional :: message progress = solver_progress_t() ! Even before initialisation, adapters can discover the expected grid size ! and configured time window to size buffers and present form defaults. progress % time_stop = session % cfg % time_stop progress % dt = session % cfg % dt progress % n_point = session % cfg % n_cell + 1 progress % is_initialized = session % has_runtime if (session % has_runtime) then progress % iteration = session % ctx % iter progress % n_point = session % ctx % state % n_pt progress % sim_time = session % ctx % t progress % time_stop = session % ctx % state % cfg % time_stop progress % dt = session % ctx % state % dt progress % residual = session % ctx % state % resid_glob progress % is_finished = session % ctx % run_complete end if call finish_readonly(solver_status_ok, '', status, message) end subroutine solver_session_get_progress !> Return the current grid-point count expected by solution copy-out buffers. subroutine solver_session_get_point_count(session, n_point, status, message) type(solver_session_t), intent(in) :: session integer, intent(out) :: n_point integer, intent(out), optional :: status character(len=*), intent(out), optional :: message if (session % has_runtime) then n_point = session % ctx % state % n_pt else n_point = session % cfg % n_cell + 1 end if call finish_readonly(solver_status_ok, '', status, message) end subroutine solver_session_get_point_count !> Copy the current primitive-variable solution into caller-owned arrays. !! !! All four arrays must be sized exactly to `solver_session_get_point_count`. !! The session never returns aliased storage across the API boundary. subroutine solver_session_copy_solution(session, x, rho, u, p, status, message) type(solver_session_t), intent(in) :: session real(wp), intent(out) :: x(:), rho(:), u(:), p(:) integer, intent(out), optional :: status character(len=*), intent(out), optional :: message integer :: n_point if (.not. session % has_runtime) then call finish_readonly(solver_status_invalid_state, 'solver_session: session is not initialised', status, message) return end if n_point = session % ctx % state % n_pt if (size(x) /= n_point .or. size(rho) /= n_point .or. size(u) /= n_point .or. size(p) /= n_point) then call finish_readonly(solver_status_invalid_argument, & 'solver_session: solution buffers must match the solver grid size', status, message) return end if call copy_current_solution(session % ctx, x, rho, u, p) call finish_readonly(solver_status_ok, '', status, message) end subroutine solver_session_copy_solution !> Write the current solution in the standard `result.dat`-style text format. !! !! Completed runs also flush the opt-in performance summary here so the !! session-based CLI, Python bindings, and C ABI preserve the same timing !! behaviour as the legacy direct runtime driver. subroutine solver_session_write_result(session, filename, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in), optional :: filename integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err character(len=512) :: output_path if (.not. session % has_runtime) then call finish(session, solver_status_invalid_state, 'solver_session: session is not initialised', status, message) return end if output_path = trim(session % ctx % state % cfg % output_file) if (present(filename)) then if (len_trim(filename) > 0) output_path = trim(filename) end if call write_solution_file(session % ctx, trim(output_path), ok, err) if (.not. ok) then call finish(session, solver_status_io_error, err, status, message) return end if call report_performance_summary(session % ctx) call finish(session, solver_status_ok, '', status, message) end subroutine solver_session_write_result !> Force an immediate checkpoint write using the configured or overridden base name. subroutine solver_session_write_checkpoint(session, base, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in), optional :: base integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err character(len=512) :: checkpoint_base if (.not. session % has_runtime) then call finish(session, solver_status_invalid_state, 'solver_session: session is not initialised', status, message) return end if checkpoint_base = trim(session % ctx % state % cfg % checkpoint_file) if (present(base)) then if (len_trim(base) > 0) checkpoint_base = trim(base) end if call write_checkpoint(session % ctx % state, trim(checkpoint_base), session % ctx % t, session % ctx % iter, ok, err) if (.not. ok) then call finish(session, solver_status_io_error, err, status, message) return end if call finish(session, solver_status_ok, '', status, message) end subroutine solver_session_write_checkpoint !> Copy the last stored writable-operation error message. subroutine solver_session_last_error(session, message) type(solver_session_t), intent(in) :: session character(len=*), intent(out) :: message message = trim(session % last_error) end subroutine solver_session_last_error !> Read an integer configuration field by canonical schema key. subroutine solver_session_get_integer(session, key, value, status, message) type(solver_session_t), intent(in) :: session character(len=*), intent(in) :: key integer, intent(out) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err call config_get_integer(session % cfg, key, value, ok, err) call finish_readonly(merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_get_integer !> Read a scalar real configuration field by canonical schema key. subroutine solver_session_get_real(session, key, value, status, message) type(solver_session_t), intent(in) :: session character(len=*), intent(in) :: key real(wp), intent(out) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err call config_get_real(session % cfg, key, value, ok, err) call finish_readonly(merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_get_real !> Read a logical configuration field by canonical schema key. subroutine solver_session_get_logical(session, key, value, status, message) type(solver_session_t), intent(in) :: session character(len=*), intent(in) :: key logical, intent(out) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err call config_get_logical(session % cfg, key, value, ok, err) call finish_readonly(merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_get_logical !> Read a string or choice-token configuration field by canonical schema key. subroutine solver_session_get_string(session, key, value, status, message) type(solver_session_t), intent(in) :: session character(len=*), intent(in) :: key character(len=*), intent(out) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err call config_get_string(session % cfg, key, value, ok, err) call finish_readonly(merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_get_string !> Read a real-3 configuration field by canonical schema key. subroutine solver_session_get_real3(session, key, value, status, message) type(solver_session_t), intent(in) :: session character(len=*), intent(in) :: key real(wp), intent(out) :: value(3) integer, intent(out), optional :: status character(len=*), intent(out), optional :: message logical :: ok character(len=256) :: err call config_get_real3(session % cfg, key, value, ok, err) call finish_readonly(merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_get_real3 !> Set an integer configuration field and invalidate any live runtime. subroutine solver_session_set_integer(session, key, value, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in) :: key integer, intent(in) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message type(config_t) :: next_cfg logical :: ok character(len=256) :: err next_cfg = session % cfg call config_set_integer(next_cfg, key, value, ok, err) if (ok) then call reset_runtime(session) session % cfg = next_cfg end if call finish(session, merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_set_integer !> Set a scalar real configuration field and invalidate any live runtime. subroutine solver_session_set_real(session, key, value, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in) :: key real(wp), intent(in) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message type(config_t) :: next_cfg logical :: ok character(len=256) :: err next_cfg = session % cfg call config_set_real(next_cfg, key, value, ok, err) if (ok) then call reset_runtime(session) session % cfg = next_cfg end if call finish(session, merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_set_real !> Set a logical configuration field and invalidate any live runtime. subroutine solver_session_set_logical(session, key, value, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in) :: key logical, intent(in) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message type(config_t) :: next_cfg logical :: ok character(len=256) :: err next_cfg = session % cfg call config_set_logical(next_cfg, key, value, ok, err) if (ok) then call reset_runtime(session) session % cfg = next_cfg end if call finish(session, merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_set_logical !> Set a string or choice-token configuration field and invalidate any runtime. subroutine solver_session_set_string(session, key, value, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in) :: key character(len=*), intent(in) :: value integer, intent(out), optional :: status character(len=*), intent(out), optional :: message type(config_t) :: next_cfg logical :: ok character(len=256) :: err next_cfg = session % cfg call config_set_string(next_cfg, key, value, ok, err) if (ok) then call reset_runtime(session) session % cfg = next_cfg end if call finish(session, merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_set_string !> Set a real-3 configuration field and invalidate any live runtime. subroutine solver_session_set_real3(session, key, value, status, message) type(solver_session_t), intent(inout) :: session character(len=*), intent(in) :: key real(wp), intent(in) :: value(3) integer, intent(out), optional :: status character(len=*), intent(out), optional :: message type(config_t) :: next_cfg logical :: ok character(len=256) :: err next_cfg = session % cfg call config_set_real3(next_cfg, key, value, ok, err) if (ok) then call reset_runtime(session) session % cfg = next_cfg end if call finish(session, merge(solver_status_ok, solver_status_invalid_argument, ok), trim(err), status, message) end subroutine solver_session_set_real3 !> Tear down the live runtime context while keeping editable config state. !! !! This is called before every mutating config change so the next initialise !! pass cannot accidentally reuse arrays, timers, or procedure pointers from !! an incompatible earlier run. subroutine reset_runtime(session) type(solver_session_t), intent(inout) :: session if (session % has_runtime) then call teardown_runtime(session % ctx) session % ctx = solver_run_context_t() session % has_runtime = .false. end if end subroutine reset_runtime !> Store the last writable-operation error and mirror it to optional outputs. subroutine finish(session, status_code, err, status, message) type(solver_session_t), intent(inout) :: session integer, intent(in) :: status_code character(len=*), intent(in) :: err integer, intent(out), optional :: status character(len=*), intent(out), optional :: message session % last_error = trim(err) if (present(status)) status = status_code if (present(message)) message = trim(err) end subroutine finish !> Mirror readonly-operation status without mutating the session state. subroutine finish_readonly(status_code, err, status, message) integer, intent(in) :: status_code character(len=*), intent(in) :: err integer, intent(out), optional :: status character(len=*), intent(out), optional :: message if (present(status)) status = status_code if (present(message)) message = trim(err) end subroutine finish_readonly end module solver_session