'''
Created on 18-Jul-2024

@author: b thakur
'''
import inspect
import traceback
import random

from sympy import (latex, Symbol, 
                   symbols,
                   Integral,
                   UnevaluatedExpr as ueval,
                   )
import sympy as sp

from ._base_helper import BaseHelper

from xv.util import list_to_str
from xv.util import strip_source_code_tabs, get_function_source_code

import matplotlib.pyplot as plt
from xv.km.ui import SvgHelper   

from xv.km import SympyUnitSystem

# _thermodynamics_helper
# ThermodynamicsHelper


class ThermodynamicsHelper(BaseHelper):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        pass
    

    def get_problem_the_first_law_of_thermodynamics(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        State and derive equation for the First Law of Thermodynamics.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        The First Law of Thermodynamics states:
        
        $\\displaystyle 
        \\Delta U = Q - W
        $
        
        where:  
        $\\displaystyle \\Delta U$: Change in the internal energy of the system.  
        $\\displaystyle Q$: Heat added to the system.  
        $\\displaystyle W$: Work done by the system on its surroundings.  
        
        Explanation:  
        1. Energy Conservation: The internal energy of a system changes due to the heat added to it and the work it performs.  
        
        2. Heat ($\\displaystyle Q$): Heat is the energy transferred between the system and its surroundings due to a temperature difference.  
        
        3. Work ($\\displaystyle W$): Work is the energy transferred when the system expands or contracts against an external pressure.  
        
        4. Sign Convention:  
        $\\displaystyle Q  >  0$: Heat is added to the system.  
        $\\displaystyle Q  <  0$: Heat is removed from the system.  
        $\\displaystyle W  >  0$: Work is done by the system on its surroundings.  
        $\\displaystyle W  <  0$: Work is done on the system by its surroundings.  
        
        Special Cases:  
        Isochoric Process ($\\displaystyle \\Delta V = 0$): No work is done ($\\displaystyle W = 0$), so $\\displaystyle \\Delta U = Q$.  
        Adiabatic Process ($\\displaystyle Q = 0$): No heat exchange, so $\\displaystyle \\Delta U = -W$.  
        Isothermal Process ($\\displaystyle \\Delta U = 0$): Internal energy is constant, so $\\displaystyle Q = W$.  
        
        The First Law establishes that energy cannot be created or destroyed, only transformed or transferred.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        answer_s = f"""
        $\\displaystyle 
        \\Delta U = Q - W
        $

        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass



    def get_problem_the_second_law_of_thermodynamics(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        State and derive equation for the Second Law of Thermodynamics.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        Entropy ($\\displaystyle  S $) is a measure of the disorder or randomness of a system.
        
        Entropy change ($\\displaystyle  dS $) for a reversible process is given by:  
        
        $\\displaystyle 
        dS = \\frac{{\\delta Q_{{\\text{{rev}}}}}}{{T}}
        $  
        
        where:  
        $\\displaystyle \\delta Q_{{\\text{{rev}}}}$: Reversible heat transfer.  
        $\\displaystyle T$: Absolute temperature at which heat is transferred.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        The Second Law of Thermodynamics states that the total entropy of 
        an isolated system can never decrease over time. 
        It may remain constant for a reversible process, 
        but it always increases for irreversible processes. 
        
        """
        soln_stmts.append(soln_stmt.replace("\n", " ").strip())
                
        soln_stmt = f"""
        $\\displaystyle 
        \\Delta S_{{\\text{{total}}}} 
        = \\Delta S_{{\\text{{system}}}} 
        + \\Delta S_{{\\text{{surroundings}}}} \\geq 0
        $
        
        In a reversible process:  
           
           $\\displaystyle 
           \\Delta S_{{\\text{{total}}}} = 0
           $  
    
        In an irreversible process:  
        
           $\\displaystyle 
           \\Delta S_{{\\text{{total}}}}  >  0
           $  
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        
        <b>The First Law of Thermodynamics states:  </b>
        
        $\\displaystyle 
        dU = \\delta Q - \\delta W
        $  
        
        For a reversible process, work done is $\\displaystyle  \\delta W = P \\, dV $, so:  
        
        $\\displaystyle 
        \\delta Q_{{\\text{{rev}}}} = dU + P \\, dV
        $
        
        Substituting $\\displaystyle  \\delta Q_{{\\text{{rev}}}} $ into the entropy equation:  
        
        $\\displaystyle 
        dS = \\frac{{dU + P \\, dV}}{{T}}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        <b>Case I: Entropy for an Isolated System:  </b>
        
        In an isolated system ($\\displaystyle  Q = 0 $), there is no heat transfer, but internal processes (like mixing or diffusion) can occur. The entropy change:  
        
        $\\displaystyle 
        \\Delta S_{{\\text{{total}}}} = \\Delta S_{{\\text{{system}}}} \\geq 0
        $
        
        <b>Case II: Entropy in Irreversible Processes:  </b>
        
        For irreversible processes, some heat is dissipated as waste, and the actual heat transfer ($\\displaystyle  \\delta Q $) is less than the reversible one ($\\displaystyle  \\delta Q_{{\\text{{rev}}}} $). This inequality leads to:  
        
        $\\displaystyle 
        \\Delta S  >  \\frac{{\\delta Q}}{{T}}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        <b>Clausius Inequality: </b>  
        
        From the second law, for a cyclic process:  
        
        $\\displaystyle 
        \\oint \\frac{{\\delta Q}}{{T}} \\leq 0
        $  
        
        Equality holds for reversible processes, and inequality holds for irreversible processes.
        
        <b>Therefore, the Second Law implies:  </b>
        1. Heat cannot spontaneously flow from a colder body to a hotter body.  
        2. The entropy of the universe (isolated system) always increases in an irreversible process.  
                
        """
        soln_stmts.append(soln_stmt.strip())
                
        answer_s = f"""
        $\\displaystyle 
        \\Delta S_{{\\text{{total}}}} 
        = \\Delta S_{{\\text{{system}}}} 
        + \\Delta S_{{\\text{{surroundings}}}} \\geq 0
        $

        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass


    def get_problem_the_third_law_of_thermodynamics(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        State and derive equation for the Third Law of Thermodynamics.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        The Third Law of Thermodynamics states:
        
        As the temperature of a system approaches absolute zero ($\\displaystyle T \\to 0
        $), the entropy ($\\displaystyle S
        $) of a perfect crystalline substance approaches a constant minimum value, which is typically zero.  
        
        $\\displaystyle 
        \\lim_{{T \\to 0}} S = S_0
        $

        where $\\displaystyle S_0 = 0$ for a perfect crystalline substance at absolute zero.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        It means:
          
        1. At absolute zero, a perfect crystal has its particles arranged in a perfectly ordered structure. There is only one possible microstate ($\\displaystyle \\Omega = 1$), and the entropy is given by:
        
        $\\displaystyle 
        S = k_B \\ln \\Omega
        $
        
        where $\\displaystyle k_B$ is the Boltzmann constant. For $\\displaystyle \\Omega = 1$, $\\displaystyle S = 0$.
        
        2. For real substances, imperfections in the crystal or residual entropy may exist due to disorder, so $\\displaystyle S_0$ might not be exactly zero.
                
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        
        Consequences:  
        1. Entropy at Zero Kelvin:
        The entropy change ($\\displaystyle \\Delta S$) for any reversible process becomes negligible as $\\displaystyle T \\to 0$, and no finite process can reach absolute zero.
        
        2. Thermodynamic Properties:
        The heat capacities $\\displaystyle C_P$ and $\\displaystyle C_V$, as well as thermal expansion coefficients, approach zero as $\\displaystyle T \\to 0$.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        answer_s = f"""
        The entropy of a pure, perfect crystalline substance at 0 K is zero.

        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass


    def get_problem_rel_onsager_reciprocal_rels_and_thermo_flows(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        Describe the relation between Onsager reciprocal relations and thermodynamic flows.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        <b> Onsager Reciprocal Relations and Thermodynamic Flows </b> 
        
        The Onsager reciprocal relations describe a fundamental symmetry in the linear response of thermodynamic systems near equilibrium. These relations apply to irreversible processes, where fluxes (flows of energy, matter, or other quantities) are driven by thermodynamic forces (gradients of temperature, chemical potential, etc.).
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        <b> Onsager Relations </b> 
        
        In a system with multiple coupled thermodynamic processes, the generalized fluxes $\\displaystyle  J_i $ and forces $\\displaystyle  X_j $ are related linearly:
        
        $\\displaystyle 
        J_i = \\sum_j L_{{ij}} X_j
        $
        
        where:
        $\\displaystyle  J_i $: Generalized flux (e.g., heat flow, particle flow),
        $\\displaystyle  X_j $: Thermodynamic force driving the flux (e.g., temperature gradient, concentration gradient),
        $\\displaystyle  L_{{ij}} $: Phenomenological coefficients that describe the coupling between flux $\\displaystyle  J_i $ and force $\\displaystyle  X_j $.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        The Onsager reciprocal relations state that:
        
        $\\displaystyle 
        L_{{ij}} = L_{{ji}}
        $
        
        This symmetry reflects the microscopic reversibility of underlying physical processes.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        <b> Thermodynamic Forces and Flows </b> 
        
        1. Thermodynamic Forces ($\\displaystyle  X_i $):  
           Thermodynamic forces arise from gradients in intensive properties:
        &emsp;$\\bullet$ Heat flow: $\\displaystyle  X = \\nabla \\frac{{1}}{{T}} $ (temperature gradient),
        &emsp;$\\bullet$ Particle diffusion: $\\displaystyle  X = \\nabla \\mu $ (chemical potential gradient),
        &emsp;$\\bullet$ Electric current: $\\displaystyle  X = \\nabla \\phi $ (electric potential gradient).
        
        2. Thermodynamic Fluxes ($\\displaystyle  J_i $):  
           Fluxes are the flows of quantities driven by thermodynamic forces:
        &emsp;$\\bullet$ Heat flow ($\\displaystyle  J_q $): Flow of thermal energy,
        &emsp;$\\bullet$ Diffusion flow ($\\displaystyle  J_D $): Flow of particles or mass,
        &emsp;$\\bullet$ Electric current ($\\displaystyle  J_E $): Flow of charge.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        <b> Examples of Coupled Flows </b> 
        
        1. Thermoelectric Effects:  
        In a material where temperature and electrical potential are coupled:
        &emsp;$\\bullet$ Heat flow ($\\displaystyle  J_q $) is driven by a temperature gradient ($\\displaystyle  \\nabla T $) and electric potential gradient ($\\displaystyle  \\nabla \\phi $).
        &emsp;$\\bullet$ Electrical current ($\\displaystyle  J_E $) is driven by the same forces.
        
        The coupled equations are:
        
        $\\displaystyle 
        J_q = L_{{qq}} \\nabla T + L_{{qE}} \\nabla \\phi
        $
        
        $\\displaystyle 
        J_E = L_{{Eq}} \\nabla T + L_{{EE}} \\nabla \\phi
        $
        
        By Onsager reciprocity:
        
        $\\displaystyle 
        L_{{qE}} = L_{{Eq}}
        $
        
        2. Chemical Reactions and Diffusion:  
        In a reacting system:
        &emsp;$\\bullet$ Chemical flux ($\\displaystyle  J_{{\\text{{chem}}}} $) can drive particle diffusion ($\\displaystyle  J_D $) and vice versa.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
         <b> Microscopic Basis of Onsager Relations </b> 
        
        The reciprocal relations arise from the microscopic reversibility of physical laws:
        &emsp;$\\bullet$ The equations governing particle motion (e.g., Newtonian mechanics, quantum mechanics) are symmetric with respect to time reversal.
        &emsp;$\\bullet$ This symmetry is reflected in the macroscopic phenomenological coefficients $\\displaystyle  L_{{ij}} $.
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        <b> Importance of Onsager Relations </b> 
        
        1. Thermodynamic Consistency:  
           Onsager's relations ensure that the description of coupled processes remains consistent with the second law of thermodynamics.
        
        2. Practical Applications:  
        &emsp;$\\bullet$ Thermoelectrics: Seebeck and Peltier effects,
        &emsp;$\\bullet$ Heat engines: Coupling between heat and mechanical work,
        &emsp;$\\bullet$ Electrochemical systems: Coupling of ionic and electrical transport.
        
        3. Generalization of Transport Phenomena:  
           Onsager's framework unifies descriptions of diverse transport processes (heat, mass, charge) under a common formalism.
        
        These relations have been validated experimentally in various systems and are fundamental to the study of irreversible thermodynamics.                
        """
        soln_stmts.append(soln_stmt.strip())
                
        answer_s = f"""
        The Onsager reciprocal relations describe a fundamental symmetry in the linear response of thermodynamic systems near equilibrium. These relations apply to irreversible processes, where fluxes (flows of energy, matter, or other quantities) are driven by thermodynamic forces (gradients of temperature, chemical potential, etc.).

        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass


    def get_problem_molar_heat_capacities(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        Define Molar Heat Capacities.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        Molar heat capacities are thermodynamic 
        properties that measure the amount of heat required 
        to raise the temperature of one mole of a substance 
        by $\\displaystyle 1 \\, \\text{{K}}$ (or $\\displaystyle 1 \\, \\circ \\text{{C}} $) 
        under specific conditions. 
        <br/>
        <br/>
        They are expressed in units of $\\displaystyle  
        \\text{{J}} \\cdot \\text{{mol}}^{{-1}} \\cdot \\text{{K}}^{{-1}} $.
        
        """
        soln_stmts.append(soln_stmt.replace("\n", " ").strip())
        
        soln_stmt = f"""
        <b>Molar Heat Capacity at Constant Volume ($\\displaystyle  C_V $):</b>
        The heat required to increase the temperature of one mole of a substance by $
        \\displaystyle 1 \\, \\text{{K}}$ when the volume is kept constant.
        $\\displaystyle 
        C_V = \\left( \\frac{{\\delta Q}}{{dT}} \\right)_V
        $
        
        where $\\displaystyle  \\delta Q 
        $ is the heat supplied, and $\\displaystyle  dT $ is the temperature change.
        
        At constant volume, the system does no work ($\\displaystyle  W = P \\, dV = 0 
        $), so the heat supplied only increases the internal energy ($\\displaystyle  U $):
        
        $\\displaystyle 
        \\delta Q = dU \\quad \\Rightarrow \\quad dU = C_V \\, dT
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        <b>Molar Heat Capacity at Constant Pressure ($\\displaystyle  C_P $):</b>
        
        Defined as the heat required to increase the temperature of one mole of a substance by $
        \\displaystyle 1 \\, \\text{{K}}$ when the pressure is kept constant.
        
        $\\displaystyle 
        C_P = \\left( \\frac{{\\delta Q}}{{dT}} \\right)_P
        $
        
        At constant pressure, the heat supplied increases the enthalpy ($\\displaystyle  H $):
        
        $\\displaystyle 
        \\delta Q = dH \\quad \\Rightarrow \\quad dH = C_P \\, dT
        $
        """
        soln_stmts.append(soln_stmt.strip())
                
        answer_s = f"""
        $\\displaystyle 
        C_V = \\left( \\frac{{\\delta Q}}{{dT}} \\right)_V
        $

        $\\displaystyle 
        C_P = \\left( \\frac{{\\delta Q}}{{dT}} \\right)_P
        $
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass


    def get_problem_molar_heat_capacity_and_universal_gas_constant(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        Find the relation between molar heat capacity and universal gas constant.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        Given:
        $\\displaystyle  C_P $: Molar heat capacity at constant pressure
        $\\displaystyle  C_V $: Molar heat capacity at constant volume
        $\\displaystyle  R $: Universal gas constant

        For one mole of an ideal gas:
        $\\displaystyle  
        PV = RT 
        \\qquad \\cdots \\text{{ (i) }}
        $ 
        
        The first law of thermodynamics: 
        
        $\\displaystyle 
        dQ = dU + dW
        \\\\

        \\implies dU =  dQ - dW
        \\\\

        \\implies dU =  dQ - PdV
        \\qquad \\cdots \\text{{ (ii) }}
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        For a perfect gas, the internal energy $\\displaystyle  U 
        $ depends only on temperature, so 
        
        $\\displaystyle  
        dU = C_V \\, dT 
        \\qquad \\cdots \\text{{ (iii) }}
        $

        At constant pressure, the heat supplied is related to the enthalpy change $\\displaystyle  
        dH $.
        
        $\\displaystyle  
        dH = C_P \\, dT 
        \\qquad \\cdots \\text{{ (iv) }}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        For a perfect gas: 
        $\\displaystyle  
        H = U + PV 
        \\qquad \\cdots \\text{{ (v) }}
        $
        
        $\\displaystyle 
        \\implies H = U + RT
        \\qquad \\text{{ from (i), }} \\quad  PV = RT
        $
        
        Differentiating this expression:
        
        $\\displaystyle 
        \\implies dH = dU + R \\, dT
        $
        
        $\\displaystyle 
        \\implies dH = C_V \\, dT + R \\, dT
        \\qquad \\text{{ from (iii), }} \\quad dU = C_V \\, dT
        $
        
        $\\displaystyle 
        \\implies \\frac{{ dH }}{{dT}}  = C_V  + R 
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        
        $\\displaystyle 
        \\implies C_P = C_V + R
        \\qquad \\text{{ from (iv), }} \\quad  C_P = \\frac{{dH}}{{dT}} 
        $
        
        $\\displaystyle 
        \\implies C_P - C_V = R
        \\qquad \\cdots \\text{{ (vi) }}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
                
        answer_s = f"""
        $\\displaystyle 
        C_P - C_V = R
        $
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass


    def get_problem_derive_molar_heat_capacity_of_polytropic_process(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        Derive the expression for the molar heat capacity $C$ in a polytropic process.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        The first law of thermodynamics states:
        
        $\\displaystyle 
        \\mathrm{{d}}Q = \\mathrm{{d}}U + \\mathrm{{d}}W,
        $

        where:
        $\\displaystyle \\mathrm{{d}}Q$ is the heat added,
        $\\displaystyle \\mathrm{{d}}U = C_V \\mathrm{{d}}T$ is the change in internal energy,
        $\\displaystyle \\mathrm{{d}}W = P \\mathrm{{d}}V$ is the work done.
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        For an infinitesimal change in a polytropic process, we have:
        
        $\\displaystyle 
        \\mathrm{{d}}Q = C \\mathrm{{d}}T,
        $
        
        where $\\displaystyle C$ is the heat capacity for the polytropic process.
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        For a polytropic process:

        $\\displaystyle 
        P V^n = \\text{{constant}},
        $
        
        where $\\displaystyle n$ is the polytropic index.

        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        Differentiating both sides:
        
        $\\displaystyle 
        n V^{{n-1}} \\mathrm{{d}}V + V^n \\mathrm{{d}}P = 0.
        $
        
        Dividing through by $\\displaystyle V^n$:
        
        $\\displaystyle 
        n \\frac{{\\mathrm{{d}}V}}{{V}} + \\frac{{\\mathrm{{d}}P}}{{P}} = 0.
        $
        
        Rearrange to express $\\displaystyle \\frac{{\\mathrm{{d}}P}}{{P}}$:
        
        $\\displaystyle 
        \\frac{{\\mathrm{{d}}P}}{{P}} = -n \\frac{{\\mathrm{{d}}V}}{{V}}.
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        From the ideal gas law:
        
        $\\displaystyle 
        P V = RT.
        $

        Differentiating it:
        
        $\\displaystyle 
        P \\mathrm{{d}}V + V \\mathrm{{d}}P = R \\mathrm{{d}}T.
        $
        
        Substitute $\\displaystyle \\frac{{\\mathrm{{d}}P}}{{P}} = -n \\frac{{\\mathrm{{d}}V}}{{V}}$ into this equation:
        
        $\\displaystyle 
        \\implies P \\mathrm{{d}}V - n V \\frac{{\\mathrm{{d}}V}}{{V}} = R \\mathrm{{d}}T.
        $
        
        $\\displaystyle 
        \\implies P \\mathrm{{d}}V - nP \\mathrm{{d}}V = R \\mathrm{{d}}T.
        $
        
        $\\displaystyle 
        \\implies (1 - n) P \\mathrm{{d}}V = R \\mathrm{{d}}T.
        $

        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        Substitute Work and Internal Energy:
        
        &emsp;$\\bullet$ The work done, $\\displaystyle \\mathrm{{d}}W = P \\mathrm{{d}}V$, gives:
        
        $\\displaystyle 
        \\mathrm{{d}}W = \\frac{{R}}{{1 - n}} \\mathrm{{d}}T.
        $
        
        &emsp;$\\bullet$ The change in internal energy, $\\displaystyle \\mathrm{{d}}U = C_V \\mathrm{{d}}T$.
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        First Law with Polytropic Heat Capacity:
        
        Substitute into $\\displaystyle \\mathrm{{d}}Q = \\mathrm{{d}}U + \\mathrm{{d}}W$:
        
        $\\displaystyle 
        \\mathrm{{d}}Q = C_V \\mathrm{{d}}T + \\frac{{R}}{{1 - n}} \\mathrm{{d}}T.
        $
        
        Factor out $\\displaystyle \\mathrm{{d}}T$:
        
        $\\displaystyle 
        \\mathrm{{d}}Q = \\left(C_V + \\frac{{R}}{{1 - n}}\\right) \\mathrm{{d}}T.
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        Express $\\displaystyle R$ in Terms of $\\displaystyle C_P$ and $\\displaystyle C_V$:

        From the relationship $\\displaystyle C_P - C_V = R$, substitute $\\displaystyle R$ into the equation:
        
        $\\displaystyle 
        \\mathrm{{d}}Q = \\left(C_V + \\frac{{C_P - C_V}}{{1 - n}}\\right) \\mathrm{{d}}T.
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        Simplify the expression for $\\displaystyle C$:

        $\\displaystyle 
        C = C_V + \\frac{{C_P - C_V}}{{1 - n}}.
        $
        
        Combine into a single fraction:
        
        $\\displaystyle 
        C = \\frac{{C_P - nC_V}}{{1 - n}}.
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""

        """
        soln_stmts.append(soln_stmt.strip())
        
        soln_stmt = f"""
        Therefore, 
         
        $\\displaystyle 
        C = \\frac{{C_P - n C_V}}{{1 - n}}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
                
        answer_s = f"""
        $\\displaystyle 
        C = \\frac{{C_P - n C_V}}{{1 - n}}
        $
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass


    def get_problem_polytropic_process(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())


        prob_s = f"""
        Define a polytropic process. How does variable $n$ changes the nature of the process.
        """
        prob_s = prob_s.replace("\n", " ").strip()

        soln_stmts = []

        soln_stmt = f"""
        A polytropic process is a thermodynamic process that follows the relation:
        
        $\\displaystyle
        P V^n = C
        $
        
        where:
        \\(P\\) is the pressure,
        \\(V\\) is the volume,
        \\(n\\) is the polytropic index,
        \\(C\\) is a constant specific to the process.
        
        """
        soln_stmts.append(soln_stmt.strip())
        
                
        soln_stmt = f"""
        $\\displaystyle 
        
        P V^n = C
        
        \\implies
        \\begin{{cases}}

        n = 0 &\\implies P V^0 = C  \\\\
              &\\implies P = C   \\\\
              &\\implies P = \\text{{ Constant }} \\quad \\text{{ (Isobaric process) }}  
              \\\\ \\\\
              
        n = 1 &\\implies P V^1 = C   \\\\
              &\\implies PV = C   \\\\
              &\\implies RT = C   \\\\
              &\\implies T = C/R     \\\\
              &\\implies T = \\text{{ Constant }} \\quad \\text{{ (Isothermal process) }}  
              \\\\ \\\\
        
             
        n = \\gamma &\\implies P V^{{\\gamma}} = C   
              \\quad \\text{{ where }} 
              \\displaystyle  \\gamma = \\frac{{C_p}}{{C_v}} \\text{{, and }} 
              \\quad Q = 0 \\quad \\text{{ (Adiabatic process) }} 
              \\\\ \\\\
              
        n = \\infty &\\implies P V^{{\\infty}} = C   \\\\
              &\\implies V = \\text{{ Constant }} \\quad \\text{{ (Isochoric process) }}  
              \\\\ \\\\
              
              
        \\end{{cases}}
        
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
                
        soln_stmt = f"""
        <b>Analysis of $n=\\infty$:</b>
        
        When $n \\rightarrow \\infty$, the equation becomes:
        
        $\\displaystyle 
        P V^{{\\infty}}=C
         $
        
        Mathematically, $V^{{\\infty}}$ approaches infinity for $V > 1
        $, or 0 for $0 < V < 1$, unless $V
        $ remains constant. The only meaningful solution in this context is when $V
        $ is constant.
        
        This means the volume does not change during the process, regardless of how pressure varies.
        
        """
        soln_stmts.append(soln_stmt.strip())
        
                
        answer_s = f"""
        A polytropic process is a thermodynamic process that follows the relation:
        
        $\\displaystyle
        P V^n = C
        $
        
        The polytropic index (\\(n\\)) determines the nature of the process:

        \\(n = 0\\) : Isobaric process (\\(P = \\text{{constant}}\\)).
        
        \\(n = 1\\) : Isothermal process (\\(T = \\text{{constant}}\\)).
        
        \\(n = \\gamma\\) (\\(\\gamma = C_p / C_v\\)) : Adiabatic process (\\(Q = 0\\)).
        
        \\(n \\to \\infty\\) : Isochoric process (\\(V = \\text{{constant}}\\)).
        
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass

    
    def get_problem_polytropic_process_equation(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        prob_s = f"""
        Derive equation for the polytropic process.
        """
        prob_s = prob_s.replace("\n", " ").strip()

            
        soln_stmts = []

        soln_stmt = f"""
        We know:

        $\\displaystyle 
        
        \\left\\{{
        
        \\begin{{array}}{{rl}}
        
        C_V \\displaystyle = \\frac{{d U}}{{d T}} 
        & \\qquad \\cdots \\text{{ (i-a) }} 
        \\\\
        
        C \\displaystyle = \\frac{{d Q}}{{d T}} \\implies C d T = d Q
        & \\qquad \\cdots \\text{{ (i-b) }} 
        \\\\
        
        C_P-C_V = R
        & \\qquad \\cdots \\text{{ (i-c) }} 
        \\\\
        
        \\frac{{C_P}}{{C_V}} = \\gamma
        & \\qquad \\cdots \\text{{ (i-d) }} 
        \\\\
        
        d W \\displaystyle = P d V 
        & \\qquad \\cdots \\text{{ (i-e) }} 
        \\\\
        
        P V = R T \\implies P d V + V d P = R d T
        & \\qquad \\cdots \\text{{ (i-f) }} 
        \\\\ \\\\
        
        \\text{{For a polytropic process:}} \\\\
        \\displaystyle \\frac{{d Q}}{{d W}} = \\text{{ constant }} = K
        \\implies K d W = d Q
        & \\qquad \\cdots \\text{{ (i-g) }} 
        
        
        \\end{{array}}\\right.
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        From the first law of thermodynamics;
        
        $\\displaystyle 
        \\begin{{aligned}}

        & d U+d W=d Q
        \\\\ \\\\

        &\\implies  C_V d T+P d V = C d T
        \\\\ \\\\

        &\\implies  C_V \\cdot R d T+R . P d V = R . C d T
        \\qquad \\text{{ Multiplied both sides by }} R 
        \\\\ \\\\
        
        &\\implies  C_V(P d V+V d P)+R . P d V=R . C d T 
        \\qquad \\text{{ substituted from (i-f), }} \\; R d T = P d V + V d P
        \\\\ \\\\
        
        
        &\\implies  
        C_V(P d V + V d P) + (C_P - C_V) . P d V
        = (C_P - C_V) . C d T 
        \\qquad \\text{{ substituted from (i-c), }} \\; C_P - C_V = R
        \\\\ \\\\
        
        &\\implies  
        C_V . P d V + C_V . V d P + C_P . P d V - C_V . P d V
        = (C_P - C_V) . C d T 
        \\\\ \\\\
        
        &\\implies  
        C_V . V d P + C_P . P d V
        = (C_P - C_V) . C d T 
        \\\\ \\\\
        
        &\\implies  
        C_V . V d P
        = (C_P - C_V) . C d T - C_P . P d V
        
        \\end{{aligned}}
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        $\\displaystyle 
        \\begin{{aligned}}
        
        \\implies  
        V d P
        & = \\left( \\frac{{C_P}}{{C_V}} - 1 \\right) . C d T - \\frac{{C_P}}{{C_V}} . P d V
        \\qquad \\text{{ divided both sides by }} \\; C_V
        \\\\ \\\\
        
        & = ( \\gamma - 1) . C d T - \\gamma . P d V
        \\qquad \\text{{ substituted from (i-d), }} \\; \\frac{{C_P}}{{C_V}}=\\gamma
        \\\\ \\\\
        
        & = ( \\gamma - 1) . d Q - \\gamma . d W
        \\qquad \\text{{ substituted from (i-b) and (i-e) }}
        \\\\ \\\\
        
        & = ( \\gamma - 1) . K d W - \\gamma . d W
        \\qquad \\text{{ substituted from (i-g), }} \\quad d Q = K d W
        \\\\ \\\\
        
        & = \\left[ ( \\gamma - 1)  K  - \\gamma \\right] d W
        \\\\ \\\\
        
        & = \\left[ ( \\gamma - 1)  K  - \\gamma \\right] P d V
        
        \\end{{aligned}}
        $
        
        
        """
        soln_stmts.append(soln_stmt.strip())
                
        soln_stmt = f"""
        $\\displaystyle 
        \\begin{{aligned}}
        
        & \\implies  
        \\displaystyle 
        \\frac{{d P}}{{P}}
        = \\left[ ( \\gamma - 1)  K  - \\gamma \\right] \\frac{{d V}}{{V}}
        \\\\ \\\\
        
        & \\implies  
        \\displaystyle 
        \\frac{{d P}}{{P}}
        - \\left[ ( \\gamma - 1)  K  - \\gamma \\right] \\frac{{d V}}{{V}}
        = 0
        \\\\ \\\\
        
        & \\implies  
        \\displaystyle 
        \\frac{{d P}}{{P}}
        + \\left[ ( 1 - \\gamma)  K  + \\gamma \\right] \\frac{{d V}}{{V}}
        = 0
        \\\\ \\\\
        
        &\\text{{Integrate both sides }}
        \\\\ \\\\
        
        & \\implies  
        \\displaystyle 
        \\ln P+[(1-\\gamma) K+\\gamma] \\cdot \\ln V = \\ln C 
        \\qquad \\qquad \\text{{ used }} \\ln C 
        \\text{{ as integration constant so that we can remove log later. }} 
        \\\\ \\\\
        

        & \\implies  
        \\displaystyle 
        \\ln \\left( P V^{{(1-\\gamma) K+\\gamma}} \\right) 
        = \\ln C 
        \\\\ \\\\
        
        
        & \\implies  
        \\displaystyle 
        P V^{{(1-\\gamma) K+\\gamma}} = C
        \\qquad \\text{{ removed log from both sides }}
        \\\\ \\\\
        
        & \\implies  
        \\displaystyle 
        P V^n = C
        \\qquad \\text{{ assumed }} n = (1-\\gamma) K+\\gamma
        
        \\end{{aligned}}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        Therefore, in a simplified way, we can write;
        
        $\\displaystyle 
        P V^n=\\text {{ constant }}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        

        answer_s = f"""
        $\\displaystyle 
        P V^n = C
        $
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass
        

    
    def get_problem_adiabatic_process_equation(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        prob_s = f"""
        Derive equation for the adiabatic process.
        """
        prob_s = prob_s.replace("\n", " ").strip()

            
        soln_stmts = []

        soln_stmt = f"""
        We know:

        $\\displaystyle 
        
        \\left\\{{
        
        \\begin{{array}}{{rl}}
        
        C_V \\displaystyle = \\frac{{d U}}{{d T}} 
        & \\qquad \\cdots \\text{{ (i-a) }} 
        \\\\
        
        C \\displaystyle = \\frac{{d Q}}{{d T}} \\implies C d T = d Q
        & \\qquad \\cdots \\text{{ (i-b) }} 
        \\\\
        
        C_P-C_V = R
        & \\qquad \\cdots \\text{{ (i-c) }} 
        \\\\
        
        \\frac{{C_P}}{{C_V}} = \\gamma
        & \\qquad \\cdots \\text{{ (i-d) }} 
        \\\\
        
        d W \\displaystyle = P d V 
        & \\qquad \\cdots \\text{{ (i-e) }} 
        \\\\
        
        P V = R T \\implies P d V + V d P = R d T
        & \\qquad \\cdots \\text{{ (i-f) }} 
        \\\\ \\\\
        
        \\text{{For a polytropic process:}} \\\\
        \\displaystyle \\frac{{d Q}}{{d W}} = \\text{{ constant }} = K
        \\implies K d W = d Q
        & \\qquad \\cdots \\text{{ (i-g) }} 
        
        
        \\end{{array}}\\right.
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        From the first law of thermodynamics;
        
        $\\displaystyle 
        \\begin{{aligned}}

        & d U+d W = d Q
        \\\\ \\\\

        &\\implies d U+d W = 0
        \\qquad \\text{{ as heat remains constant in an adiabatic change. }} 
        \\\\ \\\\

        &\\implies  C_V d T + P d V = 0
        \\\\ \\\\

        &\\implies  C_V \\cdot R d T+R . P d V = 0
        \\qquad \\text{{ Multiplied both sides by }} R 
        \\\\ \\\\
        
        &\\implies  C_V (P d V + V d P) + R . P d V = 0 
        \\qquad \\text{{ substituted from (i-f), }} \\; R d T = P d V + V d P
        \\\\ \\\\
        
        
        &\\implies  
        C_V(P d V + V d P) + (C_P - C_V) . P d V
        = 0
        \\qquad \\text{{ substituted from (i-c), }} \\; C_P - C_V = R
        \\\\ \\\\
        
        &\\implies  
        C_V . P d V + C_V . V d P + C_P . P d V - C_V . P d V
        = 0 
        \\\\ \\\\
        
        &\\implies  
        C_V . V d P + C_P . P d V
        = 0 
        \\\\ \\\\
        
        &\\implies  
        C_V . V d P
        = - C_P . P d V
        \\\\ \\\\
        
        &\\implies  
        V d P
        = - \\frac{{C_P}}{{C_V}} . P d V
        \\qquad \\text{{ divided both sides by }} \\; C_V
        \\\\ \\\\
        
        &\\implies  
        V d P
        = - \\gamma . P d V
        \\qquad \\text{{ substituted from (i-d), }} \\; \\frac{{C_P}}{{C_V}}=\\gamma
        \\\\ \\\\
        
        &\\implies  
        V d P + \\gamma . P d V = 0

        \\end{{aligned}}
        $
        """
        soln_stmts.append(soln_stmt.strip())
        
        
                
        soln_stmt = f"""
        $\\displaystyle 
        \\begin{{aligned}}
        
        & \\implies  
        \\displaystyle 
        \\frac{{d P}}{{P}} + \\gamma \\frac{{d V}}{{V}}
        = 0
        \\qquad \\text{{ divide both sides by }} P V
        \\\\ \\\\
        
        &\\text{{Integrate both sides }}
        \\\\ \\\\
        
        & \\implies  
        \\displaystyle 
        \\ln P + \\gamma \\; \\ln V = \\ln C 
        \\qquad \\qquad \\text{{ used }} \\ln C 
        \\text{{ as integration constant so that we can remove log later. }} 
        \\\\ \\\\
        
        & \\implies  
        \\displaystyle 
        \\ln \\left( P V^{{\\gamma}} \\right) 
        = \\ln C 
        \\\\ \\\\
        
        & \\implies  
        \\displaystyle 
        P V^{{\\gamma}} = C
        \\qquad \\text{{ removed log from both sides }}

        \\end{{aligned}}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        

        answer_s = f"""
        $\\displaystyle 
        P V^{{\\gamma}} = C
        $
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass
        
    def get_problem_polytropic_process_work_done(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        prob_s = f"""
        Find the work done in a polytropic process.
        """
        prob_s = prob_s.replace("\n", " ").strip()

            
        soln_stmts = []

        soln_stmt = f"""
        The general expression for work in thermodynamics:
        
        $\\displaystyle  
        W = \\int_{{V_1}}^{{V_2}} P \\, dV 
        \\qquad \\cdots \\text{{ (i) }}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        In a polytropic process, the pressure and volume are related by:
        
        $\\displaystyle  
        P V^n = \\text{{constant}} 
        \\\\ ~ \\\\
        
        \\implies \\displaystyle   
        P = \\frac{{C}}{{V^n}} 
        \\qquad \\cdots \\text{{ (ii) }}
        $
        
        where $\\displaystyle  C $ is a constant of proportionality, and $\\displaystyle  n 
        $ is the polytropic index.
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        From (i) and (ii):
        
        $\\displaystyle  
        W = \\int_{{V_1}}^{{V_2}} \\frac{{C}}{{V^n}} \\, dV 
        \\qquad \\cdots \\text{{ (iii) }}
        $
        
        
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        <h3>Case for $\\displaystyle  n \\ne 1 $: </h3>
        From (ii), at the initial state ($\\displaystyle  P_1, V_1 $):
        
        $\\displaystyle  
        C = P_1 V_1^n 
        \\\\ ~ \\\\
        
        \\implies \\displaystyle  
        P = \\frac{{P_1 V_1^n}}{{V^n}} 
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        Substituting this expression for $\\displaystyle  P $ into the work formula:
        
        $\\displaystyle  
        \\begin {{aligned}}
        
        W 
        &\\displaystyle   = \\int_{{V_1}}^{{V_2}} \\frac{{P_1 V_1^n}}{{V^n}} \\, dV 
        \\\\ \\\\

        &\\displaystyle   = P_1 V_1^n \\int_{{V_1}}^{{V_2}} V^{{-n}} \\, dV 
        \\\\ \\\\
        
        &\\displaystyle   = P_1 V_1^n \\left[ \\frac{{V^{{1-n}}}}{{1-n}} \\right]_{{V_1}}^{{V_2}} 
        \\\\ \\\\
        
        &\\displaystyle   = \\frac{{P_1 V_1^n}}{{1-n}} \\left( V_2^{{1-n}} - V_1^{{1-n}} \\right)
        \\\\ \\\\
        
        \\end {{aligned}}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        Using the polytropic relation $\\displaystyle  P V^n = \\text{{constant}} 
        $ to replace $\\displaystyle  P_1 V_1^n $ with $\\displaystyle  P_2 V_2^n $, resulting in:
        
        $\\displaystyle  
        W = \\frac{{P_2 V_2 - P_1 V_1}}{{1-n}}, \\quad n \\neq 1 
        \\qquad \\cdots \\text{{ (iii) }}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
        
        
        soln_stmt = f"""
        <h3>Case for $\\displaystyle  n = 1 $ (isothermal process): </h3>
        If $\\displaystyle  n = 1 $, the polytropic relation becomes $\\displaystyle  
        PV = \\text{{constant}} $, and the work formula becomes:
        
        $\\displaystyle  
        W = \\int_{{V_1}}^{{V_2}} \\frac{{C}}{{V}} \\, dV 
        $
        
        The integral of $\\displaystyle  \\frac{{1}}{{V}} $ is:
        
        $\\displaystyle  
        W = P_1 V_1 \\ln \\left( \\frac{{V_2}}{{V_1}} \\right) 
        \\qquad \\cdots \\text{{ (iv) }}
        $
        
        """
        soln_stmts.append(soln_stmt.strip())
                

        answer_s = f"""
        $\\displaystyle  
        W  = 
        \\begin{{cases}}
        
        \\displaystyle 
        \\frac{{P_2 V_2-P_1 V_1}}{{1-n}}, & n \\neq 1 
        \\\\ \\\\
        
        \\displaystyle 
        P_1 V_1 \\ln \\left(\\frac{{V_2}}{{V_1}}\\right), & n=1
        
        \\end{{cases}}
        $
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass
        

    def get_problem_polytropic_process_graph(self, **kwargs):
        '''
        '''
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        index = kwargs.get('index', None)
        
        try:
            methods = [
                self.get_problem_adiabatic_process_graph,
                self.get_problem_isothermal_process_graph,
                self.get_problem_isobaric_process_graph,
                ]
            
            if ( index is None or 
                 index >= len(methods) or
                 - index > len(methods) 
               ):
                random_choices = list(range(len(methods)))
        
                identifier = f"{self.__class__.__name__}{id(self)}.{inspect.stack()[0][3]}.random_choices"
                random_choice = self.get_no_repeat_random_choice(identifier, random_choices)
            else:
                random_choice = index

            
            method = methods[random_choice]
            return method(**kwargs)
        
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            kwargs = {kwargs}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            return None
            pass
        pass

    def get_problem_adiabatic_process_graph(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        prob_s = f"""
        Plot the pressure-volume relationship for adiabatic processes with different $\gamma
        $ values.
        """
        prob_s = prob_s.replace("\n", " ").strip()

            
        soln_stmts = []

        use_cache = False
        refresh_cache = False
        plot_code, svg_str = self.call_plot_method(self._plot_adiabatic_graph,
                                                   #data,
                                                   use_cache = use_cache,
                                                   refresh_cache = refresh_cache)
        

        soln_stmt = f"""
        <b>Code to plot graph</b>
        <pre>{plot_code}</pre>
        """
        soln_stmts.append(soln_stmt.strip())
                

        answer_s = f"""
        {svg_str}
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass
        

    def _plot_adiabatic_graph(self):
        
        import numpy as np
        import matplotlib.pyplot as plt
        
        # Constants
        P0 = 100  # Initial pressure in arbitrary units
        V0 = 1    # Initial volume in arbitrary units
        
        # Different gamma values for different types of gases
        gamma_values = [1.67, 1.4, 1.3]  # Monatomic, diatomic, polyatomic gases
        labels = ['Monatomic (γ = 1.67)', 'Diatomic (γ = 1.4)', 'Polyatomic (γ = 1.3)']
        colors = ['blue', 'green', 'red']
        
        # Generate volume values
        V = np.linspace(0.1, 2, 500)
        
        # Create the plot
        plt.figure(figsize=(8, 6))
        
        for gamma, label, color in zip(gamma_values, labels, colors):
            # Calculate pressure for each gamma value
            P = P0 * (V0**gamma) / (V**gamma)
            plt.plot(V, P, label=label, color=color)
        
        plt.title("Pressure vs. Volume in Adiabatic Processes")
        plt.xlabel("Volume (V)")
        plt.ylabel("Pressure (P)")
        plt.grid(True)
        plt.legend()
        pass


        
    def get_problem_isothermal_process_graph(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        prob_s = f"""
        Plot the pressure-volume relationship for isothermal process.
        """
        prob_s = prob_s.replace("\n", " ").strip()

            
        soln_stmts = []

        use_cache = False
        refresh_cache = False
        plot_code, svg_str = self.call_plot_method(self._plot_isothermal_graph,
                                                   #data,
                                                   use_cache = use_cache,
                                                   refresh_cache = refresh_cache)
        

        soln_stmt = f"""
        <b>Code to plot graph</b>
        <pre>{plot_code}</pre>
        """
        soln_stmts.append(soln_stmt.strip())
                

        answer_s = f"""
        {svg_str}
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass
        
        
    def _plot_isothermal_graph(self):
        
        import numpy as np
        import matplotlib.pyplot as plt
        
        # Constants
        P0 = 100  # Initial pressure in arbitrary units
        V0 = 1    # Initial volume in arbitrary units
        
        # Generate volume values
        V = np.linspace(0.1, 2, 500)
        
        # Calculate pressure for isothermal process (P * V = constant)
        P = P0 * V0 / V
        
        # Create the plot
        plt.figure(figsize=(8, 6))
        plt.plot(V, P, label="Isothermal Process (P * V = const.)", color='purple')
        
        plt.title("Pressure vs. Volume in an Isothermal Process")
        plt.xlabel("Volume (V)")
        plt.ylabel("Pressure (P)")
        plt.grid(True)
        plt.legend()

        pass


        
    def get_problem_isobaric_process_graph(self, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        prob_s = f"""
        Plot the pressure-volume relationship for isobaric process.
        """
        prob_s = prob_s.replace("\n", " ").strip()

            
        soln_stmts = []

        use_cache = False
        refresh_cache = False
        plot_code, svg_str = self.call_plot_method(self._plot_isobaric_graph,
                                                   #data,
                                                   use_cache = use_cache,
                                                   refresh_cache = refresh_cache)
        

        soln_stmt = f"""
        <b>Code to plot graph</b>
        <pre>{plot_code}</pre>
        """
        soln_stmts.append(soln_stmt.strip())
                

        answer_s = f"""
        {svg_str}
        """
        answer_s = answer_s.strip()

        soln_s = "\n\n".join(soln_stmts)
        
        result = {
            'prob_s': prob_s.strip(), 
            'soln_s': soln_s.strip(), 
            'answer_s': answer_s.strip(), 
            }
        
        return result
        pass
        
        
    def _plot_isobaric_graph(self):
        
        import numpy as np
        import matplotlib.pyplot as plt
        
        # Constants
        P = 100  # Constant pressure in arbitrary units
        
        # Generate volume values
        V = np.linspace(0.5, 2, 500)  # Volume range
        
        # Create the plot
        plt.figure(figsize=(8, 6))
        plt.plot(V, [P] * len(V), label="Isobaric Process (P = const.)", color='orange')
        
        plt.title("Pressure vs. Volume in an Isobaric Process")
        plt.xlabel("Volume (V)")
        plt.ylabel("Pressure (P)")
        plt.grid(True)
        plt.legend()

        pass


    def call_plot_method(self, method, *args, **kwargs):
        '''
        returns plot_code, svg_str
        
        @method: method to be called
        
        Pass these two methods to use cache util. Any other params passed will be subsequently 
        passed to the method to be called.
        @use_cache = True/False
        @refresh_cache = False/True
        '''
        from xv.util.data import CacheProcessor
        cache_processor = CacheProcessor(verbose = self.verbose)
        plot_code, svg_str = cache_processor.get_svg_and_runnable_code(method, 
                                                                       *args, 
                                                                       **kwargs)
        plot_code = plot_code.replace("self.verbose", "False")
        return plot_code, svg_str
        pass


    def _plot_specified_method(self, method_name, **kwargs):
        plt.ioff()
        plotter = self
        
        if not hasattr(plotter, method_name):
            return None

        method = getattr(plotter, method_name)   
        plot_code = self._get_method_source_code(method)

        fig, ax, data = method(**kwargs)
        
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
        
        if not svg_str:
            if self.verbose:
                print(f"Error: svg_str could not be initialized")
            return None, plot_code
            
        plt.clf()
        plt.close()
        return svg_str, plot_code, data
        
        
        
    def _get_method_source_code(self, method, start = 1, end = -2):
        '''
        method to call. It must be a method, not name
        start: -1, line index to start
        end: -2, line index to end
        '''
        plot_code = get_function_source_code(method, 
                                             strip_tab = False)
        plot_code = plot_code[start : end]
        plot_code = strip_source_code_tabs(plot_code)
        
        plot_code.append("plt.show()")
        
        plot_code = "\n".join(plot_code)
        return plot_code
        pass


    pass
#class ThermodynamicsHelper:

    