Welcome! This online textbook is a living project. Content is being added and refined weekly as we build a complete resource for General Chemistry I & II. Thank you for visiting!
Work
Work (w) is the transfer of energy that causes organized, uniform motion in the surroundings.
This concept describes a process where the chaotic, random actions of individual particles give rise to an organized, macroscopic change. Consider the gas inside a cylinder with a movable piston. While each individual gas molecule zips around randomly, their countless collisions against the inner face of the piston produce a net, directional force.
This steady, outward force is the statistical result of the chaotic impacts. If this net force is sufficient to push the piston, the energy of the system has been used to move a macroscopic object over a distance. This uniform movement of the piston is the hallmark of work and is fundamentally different from the random, chaotic molecular motion associated with heat. This connection between a collective force and its resulting motion is the basis of work in chemistry.
Work is formally defined as the energy transferred when a force acts on an object over a distance. The familiar equation from physics is:
\[
w = F_{\mathrm{ext}}~d
\] In thermodynamics, we are almost always concerned with the change between an initial and a final state. Therefore, it is more precise to describe this movement as a displacement, or a change in position, represented by the symbol Δx. This gives us the more formal version of the work equation:
\[
w = F_{\mathrm{ext}}~\Delta x
\] where w is work, Fext is the opposing external force, and Δx is the displacement (xfinal − xinitial). For a simple, one-way process like a piston moving, the distance (d) and the displacement (Δx) are the same. We use the Δx notation because it creates a clear parallel to the pressure-volume work done by a gas.
In chemistry, there are several types of work, such as the ordered movement of electrons in a battery (electrical work). However, the most common type we will study is the work associated with a change in volume. This is called pressure-volume work or PV-work.
Pressure-Volume Work
PV-work occurs when a gas expands or is compressed against an external pressure. Consider a gas confined in a cylinder with a movable piston.
Expansion (Work done by the system): If the gas inside the cylinder expands, it pushes the piston outward against the external pressure of the surroundings. The system is doing work on the surroundings.
Compression (Work done on the system): If the surroundings exert a greater pressure, the piston moves inward, compressing the gas. The surroundings are doing work on the system.
The equation for calculating PV-work done at a constant external pressure is: \[
w = -P_{\mathrm{ext}} \Delta V
\]
where
w is the work.
Pext is the constant external pressure against which the volume change occurs.
ΔV is the change in the system’s volume (Vfinal − Vinitial).
Sign Conventions for Work
The negative sign in the PV-work equation is part of the sign convention, which is always defined from the system’s point of view.
Expansion (System does work and loses energy):
The system expands, so ΔV is positive (V_final > V_initial).
The system pushes on the surroundings, losing energy. Therefore, w must be negative.
w = -P_ext * (+ΔV) = negative value
Compression (System has work done on it and gains energy):
The system is compressed, so ΔV is negative (V_final < V_initial).
The surroundings push on the system, giving it energy. Therefore, w must be positive.
w = -P_ext * (-ΔV) = positive value
A positive value for w means energy has been “deposited” into the system’s energy account, while a negative value means energy has been “withdrawn.”
Units of Work and Energy: The Joule
The standard SI unit for work, and all forms of energy, is the Joule (J). The Joule is a derived unit, defined in terms of the base SI units for mass (kg), length (m), and time (s): \[
1~\mathrm{J} \equiv 1~\mathrm{kg} ~ \frac{\mathrm{m^2}}{\mathrm{s^2}}
\] A perfect illustration of this is the equation for kinetic energy (Ek) which is the energy an object possesses due to its motion. The kinetic energy is a function of an object’s mass (m) and velocity (v): \[
E_{\mathrm{k}} = \frac{1}{2}~m v^2
\] By analyzing the units, we can see how this equation results in Joules:
Just as with kinetic energy, when calculating pressure-volume (PV) work, we must ensure our units result in Joules. The most direct way to achieve this is to use SI units for pressure and volume from the start:
Pressure (P) in Pascals (Pa)
Volume (V) in cubic meters (m3)
If you use these units, the result of the PΔV calculation will automatically be in Joules.
Handling Common, Non-SI Units
In practice, especially in chemistry, pressure is often given in atmospheres (atm) and volume in liters (L). While convenient, multiplying these units results in an energy unit called the liter-atmosphere (L atm).
Since thermodynamic laws (like the First Law) require energy terms to be combined, work and heat must be in the same unit. Because heat is typically expressed in Joules, it is essential to convert work from L atm to Joules. This is achieved using the exact conversion factor: \[
1~\mathrm{L ~ atm} = 101.325~\mathrm{J}
\] Therefore, you can calculate work using the given units of L and atm, and then apply this conversion factor to get the final answer in Joules, ensuring it can be correctly used in further thermodynamic calculations.
NoteWhat about calories?
An older, non-SI unit for energy is the calorie (cal), originally defined as the energy needed to raise one gram of water by one degree Celsius. The calorie is now formally defined in relation to the Joule: 1 cal = 4.184 J (exactly). The “Calories” reported on food labels (Cal, with a capital C) are actually kilocalories (1 Cal = 1000 cal = 1 kcal).
Visualizing Irreversible Work
// =============================================================================// MATHJAX LOADER// =============================================================================mathjax_dependency = {returnnewPromise(resolve => {// Wait a bit to allow parent page MathJax to fully initializesetTimeout(() => {// Check if MathJax is already loaded (any version)if (window.MathJax&& (window.MathJax.typesetPromise||window.MathJax.typeset)) {console.log("MathJax already loaded by parent page - using existing instance");window.our_mathjax_loaded=true;resolve("Using existing MathJax");return; }// Only load MathJax if not already presentconsole.log("Loading MathJax for standalone widget");window.MathJax= {tex: {packages: {'[+]': ['base','ams','newcommand','noerrors','noundefined']},inlineMath: [['$','$'], ['\\(','\\)']],displayMath: [['$$','$$'], ['\\[','\\]']] },chtml: {scale:0.85,mtextInheritFont:true,adaptiveCSS:true },startup: {ready: () => {console.log("MathJax ready with enhanced packages.");window.MathJax.startup.defaultReady();window.our_mathjax_loaded=true;resolve("MathJax is ready."); } } };const script =document.createElement('script'); script.src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml-full.js'; script.async=true;document.head.appendChild(script); },100);// Wait 100ms for parent MathJax to initialize });}
d3 =require("d3@7")htl =require("htl")// =============================================================================// THERMODYNAMIC SYSTEM DOCUMENTATION// =============================================================================/*KEY THERMODYNAMIC CONCEPTS:1. IRREVERSIBLE WORK: Work done when a gas expands/compresses against constant external pressure - Formula: w = -P_ext × ΔV = -P_ext × (V_final - V_initial) - Sign convention: Negative work = energy leaving system (expansion) Positive work = energy entering system (compression)2. IDEAL GAS LAW: PV = nRT - Relates pressure, volume, temperature, and amount of gas - Assumes gas molecules have no volume and no intermolecular forces3. ISOTHERMAL PROCESS: Temperature remains constant (T = 298.15 K) - Heat exchange occurs with surroundings to maintain constant temperature - Internal energy change (ΔU) = 0 for ideal gasSYSTEM ASSUMPTIONS:1. Ideal gas behavior follows PV = nRT exactly2. Isothermal process at 298.15 K (25°C)3. Constant external pressure during expansion/compression4. Frictionless piston movement5. System reaches equilibrium with external pressure at final state6. No heat exchange effects explicitly modeled (temperature remains constant)7. Standard thermodynamic sign convention: work done by system is negativeEDUCATIONAL CONTEXT:This visualization demonstrates how real piston-cylinder systems behave whenthe external pressure differs from the system pressure. Unlike reversibleprocesses where P_ext ≈ P_system at every step, irreversible processes havea sudden pressure change, making the work calculation simpler (constant P_ext).*/// ==================== CONSTANTS ====================R_SI =8.314472// J/(mol·K) - Universal gas constant (SI units)R_atm =0.08314472// L·atm/(mol·K) - Universal gas constant (convenient units)L_atm_to_kJ =0.101325// Exact conversion factor: 1 L·atm = 0.101325 kJn_moles =1.000// mol (exact) - Amount of gas in the systemT_kelvin =298.15// K (exact, isothermal) - Constant temperature throughout process// Initial state - calculated from ideal gas law: V = nRT/PP_initial =4.00// atm - Starting system pressure (clear reference point)V_initial = (n_moles * R_atm * T_kelvin) / P_initial // Initial volume from PV = nRT (~6.12 L)// ==================== SPRING SYSTEM CONSTANTS ====================/*SPRING-BLOCK SYSTEM ANALOGY:The spring system provides a mechanical analogy to the gas system:- Force (F) ↔ Pressure (P)- Displacement (x) ↔ Volume (V)- Spring constant (k) ↔ Gas compressibility- Work: w = F × Δx (similar to w = -P × ΔV)PHYSICAL INTERPRETATION:- Spring compressed by external force (like gas compressed by external pressure)- Work done when spring moves between equilibrium positions- Sign convention: Positive work = energy entering system (compression)PHYSICAL CONSTRAINTS:- Maximum extension: 0.36 m beyond rest position (for visualization limits)- Minimum theoretical force: F_min = -k × 0.36 = -90 N (spring pulling)- However, we constrain the system to show realistic behavior*/k_spring =250.0;// N/m - Spring constant (stiff spring for visible compression)x_rest =1.0;// m - Spring's natural resting positionF_initial =50;// N - Initial compressing force on spring (reduced for better zero point)x_initial = x_rest - (F_initial / k_spring);
// ==================== WIDGET ===================={// Dependencies mathjax_dependency; workWidgetStyles;const container = htl.html`<div id="work-widget-container" class="work-widget-container"></div>`;// Local constants needed within widget scopeconst x_rest_local =1.0;// m - Spring's natural resting positionconst k_spring_local =250.0;// N/m - Spring constantconst F_initial_local =50;// N - Initial compressing force on spring (reduced for better zero point)const x_initial_local = x_rest_local - (F_initial_local / k_spring_local);// m - Initial displacement (~0.8 m)// ==================== HELPER FUNCTIONS ====================functioncountSigFigs(numStr) {if (!numStr ||typeof numStr !=='string') returnnull;let str = numStr.trim().replace(/^[+\-]/,'');if (str.includes('e') || str.includes('E')) {const parts = str.split(/[eE]/); str = parts[0]; } str = str.replace(/^0+\./,'0.');if (str.includes('.')) { str = str.replace(/^0+/,'');if (str.startsWith('.')) str ='0'+ str;return str.replace('.','').replace(/^0+/,'').length; } else { str = str.replace(/^0+/,'') ||'0';if (str ==='0') return1;const trailingZeros = str.match(/0+$/);if (trailingZeros) {return str.length; }return str.replace(/0+$/,'').length; } }functionroundBankers(num, sf) {if (num ===0) return0;if (!sf || sf <1) return num;const magnitude =Math.pow(10, sf -Math.floor(Math.log10(Math.abs(num))) -1);const scaled = num * magnitude;const floorScaled =Math.floor(scaled);const diff = scaled - floorScaled;let roundedScaled;if (diff ===0.5) { roundedScaled = (floorScaled %2===0) ? floorScaled : floorScaled +1; } else { roundedScaled =Math.round(scaled); }return roundedScaled / magnitude; }functiongetDecimalPlaces(num, sf) {// Determine the number of decimal places in a number when rounded to sf sig figs// This is used for addition/subtraction precision trackingif (num ===0) return sf -1;// For zero, use sf-1 decimal placesconst rounded =roundBankers(num, sf);const str = rounded.toPrecision(sf);if (str.includes('e')) {// Handle scientific notationconst parts = str.split('e');const decimals = parts[0].includes('.') ? parts[0].split('.')[1].length:0;const exponent =parseInt(parts[1]);returnMath.max(0, decimals - exponent); }if (str.includes('.')) {return str.split('.')[1].length; }return0; }functiongetInputWithBar(num, sf, decimalPlaces =null) {// Format input values with bar over last significant digit (no guard digits)// e.g., 50.0 with sf=3 → 50.\bar{0}// If decimalPlaces is provided, use that for precision instead of sf// Check if number is effectively zero (accounting for floating point errors)const isZero =Math.abs(num) <1e-10;if (isZero || num ===0) {if (decimalPlaces !==null) {// Use decimal places for zero values from subtraction// e.g., decimalPlaces=2 → 0.0\bar{0}if (decimalPlaces ===0) return`\\bar{0}`;const zeros ='0'.repeat(decimalPlaces -1);return`0.${zeros}\\bar{0}`; }if (sf >=2) {const zeros ='0'.repeat(sf -2);return`0.\\bar{0}${zeros}`; } elseif (sf ===1) {return`\\bar{0}`; } else {return"0"; } }const precisionStr = num.toPrecision(sf);let significantPart = precisionStr;let exponentPart ='';if (precisionStr.includes('e')) {const parts = precisionStr.split('e'); significantPart = parts[0]; exponentPart ='e'+ parts[1]; }const barredPart =`${significantPart.slice(0,-1)}\\bar{${significantPart.slice(-1)}}`;return`${barredPart}${exponentPart}`; }functiongetUnroundedWithBar(num, sf, decimalPlaces =null) {// Format intermediate calculated values with bar and TWO guard digits// e.g., 0 with sf=3 → 0.\bar{0}00 (showing 2 guard digits)// If decimalPlaces is provided, use that for precision instead of sf// Check if number is effectively zero (accounting for floating point errors)const isZero =Math.abs(num) <1e-10;if (isZero || num ===0) {if (decimalPlaces !==null) {// Use decimal places for zero values from subtraction// e.g., decimalPlaces=2 → 0.0\bar{0}00if (decimalPlaces ===0) return`\\bar{0}00`;const zeros ='0'.repeat(decimalPlaces -1);return`0.${zeros}\\bar{0}00`; }if (sf >=2) {const zeros ='0'.repeat(sf -2);return`0.\\bar{0}${zeros}00`;// Add two guard digits } elseif (sf ===1) {return`\\bar{0}00`;// Add two guard digits } else {return"0"; } }const precisionStr = num.toPrecision(sf);const fullStr = num.toPrecision(sf +2);const guardDigits = fullStr.substring(precisionStr.length);let significantPart = precisionStr;let exponentPart ='';if (precisionStr.includes('e')) {const parts = precisionStr.split('e'); significantPart = parts[0]; exponentPart ='e'+ parts[1]; }const barredPart =`${significantPart.slice(0,-1)}\\bar{${significantPart.slice(-1)}}`;return`${barredPart}${guardDigits}${exponentPart}`; }functionformatNumberForHTML(num, sf) {// Format number with proper HTML minus sign for HTML/SVG text elementsconst rounded =roundBankers(num, sf).toPrecision(sf);return rounded.startsWith('-') ? rounded.replace('-','−') : rounded; }// State managementlet state = {mode:'gas',// 'gas' or 'spring' - current visualization modeP_ext: P_initial,// atm - start at initial pressure (neutral point)F_ext: F_initial_local,// N - external force for spring systemP_initial: P_initial,V_initial: V_initial,F_initial: F_initial_local,x_initial: x_initial_local,isResetting:false };// ==================== PROCESS EXPLANATION CONTENT ====================const processExplanations = {gas: {negative: {title:"Expansion",blurb:"The internal pressure of the gas is greater than the external pressure. The gas expands, pushing the piston outward and doing work on the surroundings. Because the system loses energy by doing work, the sign of <i>w</i> is <b>negative</b>." },positive: {title:"Compression",blurb:"The external pressure is greater than the internal pressure of the gas. The surroundings push the piston inward, compressing the gas and doing work on the system. Because the system gains energy from the work done on it, the sign of <i>w</i> is <b>positive</b>." },zero: {title:"No Work Done",blurb:"No work is performed. This occurs either because the initial and final volumes are the same (<b>Δ<i>V</i> = 0</b>), or because the gas is expanding into a vacuum (<b>P<sub>ext</sub> = 0</b>). In either case, no force has moved a distance against opposition." } },spring: {negative: {title:"Expansion (Work Done by Spring)",blurb:"The spring expands from compression, releasing stored elastic potential energy to do work on the surroundings. Work is negative (<i>w</i> < 0) because energy leaves the system as the spring pushes against the external force." },positive: {title:"Compression (Work Done on Spring)",blurb:"The external force compresses the spring to a new equilibrium position. Work is positive (<i>w</i> > 0) because the surroundings add energy to the system, increasing the spring's stored elastic potential energy." },zero: {title:"Free Expansion",blurb:"Free expansion to the spring's natural equilibrium (rest length). No work is done because work requires a force acting through a distance against opposition. Without external force, the spring simply releases stored energy as kinetic motion." } } };// Calculate derived values based on thermodynamic/mechanical principlesfunctioncalculateState(controlValue) {/* CALCULATION METHOD: GAS MODE: 1. Final volume from ideal gas law: V_final = nRT/P_ext 2. Final pressure equals external pressure (equilibrium condition) 3. Volume change: ΔV = V_final - V_initial 4. Work calculation: w = -P_ext × ΔV (irreversible work formula) 5. Unit conversion: L·atm → kJ using exact conversion factor SPRING MODE: 1. Final displacement from Hooke's law: x_final = x_rest - F_ext/k 2. Final force equals external force (equilibrium condition) 3. Displacement change: Δx = x_final - x_initial 4. Work calculation: w = F_ext × Δx (mechanical work formula) 5. Unit: Joules (no conversion needed) */if (state.mode==='gas') {const P_ext = controlValue;const V_final = (n_moles * R_atm * T_kelvin) / P_ext;// From PV = nRTconst P_final = P_ext;// System equilibrates to external pressure at final stateconst deltaV = V_final - V_initial;// Volume change (positive = expansion, negative = compression)const work_Latm =-P_ext * deltaV;// Work in L·atm: w = -P_ext × ΔVconst work_kJ = work_Latm * L_atm_to_kJ;// Convert to kJ for more intuitive units// Significant figures: P_ext from slider has 3 sig figs (e.g., "2.50")const sigFigs =3;// Decimal places for subtraction results (addition/subtraction uses decimal places, not sig figs)const deltaV_decimalPlaces =Math.min(getDecimalPlaces(V_final, sigFigs),getDecimalPlaces(V_initial,3));// For multiplication involving values from subtraction, when result is near zero,// track decimal places based on the subtraction resultconst work_Latm_decimalPlaces = (Math.abs(work_Latm) <1e-10) ? deltaV_decimalPlaces :null;const work_kJ_decimalPlaces = (Math.abs(work_kJ) <1e-10) ? deltaV_decimalPlaces :null;return { P_final, V_final, deltaV, deltaV_decimalPlaces, work_Latm, work_Latm_decimalPlaces, work_kJ, work_kJ_decimalPlaces,work_J: work_kJ *1000,// Convert to Joules for consistency with spring sigFigs,isExpansion: P_ext < P_initial,// Gas expands when external pressure is lowerisCompression: P_ext > P_initial // Gas compresses when external pressure is higher }; } elseif (state.mode==='spring') {const F_ext = controlValue;const k = k_spring_local;// Use fixed spring constant from CONSTANTS section// Calculate theoretical position using the same scaling as visualizationconst theoreticalX = springMinBlockX + (maxExtensionDistance - (F_ext / k)) / visualToPhysicalScale;// Apply visual constraints - keep within animation window (same as visualization)const maxBlockRight = springWidth -25;// Keep 25px from right edgeconst maxBlockX = maxBlockRight - springBlockWidth;const minBlockX = springMinBlockX;// Minimum position from wall// Constrained final positionconst x_final_constrained =Math.max(minBlockX,Math.min(maxBlockX, theoreticalX));// Convert visual position back to physical coordinates for table displayconst actualCompression = maxExtensionDistance - (x_final_constrained - springMinBlockX) * visualToPhysicalScale;const F_final =Math.round(k * actualCompression *100) /100;// Round to 2 decimal places to avoid floating point errors// Convert final position back to physical coordinates for table display const x_final_physical = x_rest_local - actualCompression;const deltaX = x_final_physical - x_initial_local;// Displacement change in physical coordinates// For work calculation, use the external force (controlValue) since that's the force doing work on the system// The external force acts in the direction of compression (negative x direction)// So work = external_force × displacement (where displacement is negative for compression)const work_J =-controlValue * deltaX;// Work in Joules: w = F_ext × Δx (negative because F_ext acts opposite to positive x)// Significant figures: F_ext from slider has 3 sig figsconst sigFigs =3;// Decimal places for subtraction results (addition/subtraction uses decimal places, not sig figs)const deltaX_decimalPlaces =Math.min(getDecimalPlaces(x_final_physical, sigFigs),getDecimalPlaces(x_initial_local,3));const work_kJ_val = work_J /1000;// For multiplication involving values from subtraction, when result is near zero,// track decimal places based on the subtraction resultconst work_J_decimalPlaces = (Math.abs(work_J) <1e-10) ? deltaX_decimalPlaces :null;const work_kJ_decimalPlaces = (Math.abs(work_kJ_val) <1e-10) ? deltaX_decimalPlaces :null;return { F_final,x_final: x_final_physical,x_final_theoretical: x_rest_local - (controlValue / k),// Keep theoretical for reference deltaX, deltaX_decimalPlaces, work_J, work_J_decimalPlaces,work_kJ: work_kJ_val,// Convert to kJ for display consistency work_kJ_decimalPlaces, sigFigs,isExpansion: F_final < F_initial_local,// Spring extends when actual force is lower than initialisCompression: F_final > F_initial_local,// Spring compresses when actual force is higher than initialisConstrained: x_final_constrained !== theoreticalX // Flag if constraints are active }; } }// Create main layout with educational contextconst titleDiv = htl.html`<div></div>`;// Will be populated dynamically by updateWidgetTitle()/* VISUALIZATION COMPONENTS: 1. PISTON ANIMATION: - Shows gas volume change as piston moves up/down - Gas color indicates process type: blue (expansion), orange (compression) - External pressure arrow shows force applied to piston 2. P-V DIAGRAM: - Plots pressure vs. volume for the process - Blue point: Initial state (P₁, V₁) - Red point: Final state (P₂, V₂) - Shaded area: Work done (rectangular for constant P_ext) - Vertical arrow: Pressure change from P₁ to P_ext 3. INTERACTIVE CONTROLS: - Slider to adjust external pressure (1.00 - 8.00 atm) - Real-time updates of all visualizations - Reset button to return to initial conditions EDUCATIONAL NOTES: - When P_ext < P_initial: Gas expands, does work on surroundings - When P_ext > P_initial: Gas compresses, work done on gas - The rectangular area in P-V diagram represents work (easier to calculate than curved area for reversible processes) */// Create mode toggleconst toggleContainer = htl.html`<div style="text-align: center; margin-bottom: 20px;"> <button id="gas-mode-btn" class="mode-button active">Gas/Piston System</button> <button id="spring-mode-btn" class="mode-button">Spring Analogy</button> </div>`;const visualsDiv = htl.html`<div class="visuals-container"></div>`;const pistonDiv = htl.html`<div class="piston-container"></div>`;const springDiv = htl.html`<div class="spring-container"></div>`;// Container for spring SVGconst pvDiagramDiv = htl.html`<div class="pv-diagram-container"></div>`; visualsDiv.append(pistonDiv, springDiv, pvDiagramDiv);const controlsDiv = htl.html`<div class="controls-container"></div>`;const sliderContainer = htl.html`<div></div>`;// Dynamic container for slidersconst processExplanationDiv = htl.html`<div class="process-explanation-container"></div>`;// Process explanation areaconst stateTableDiv = htl.html`<div class="state-table-container"></div>`;const workCalcDiv = htl.html`<div class="work-calc-container"></div>`; container.append(titleDiv, toggleContainer, visualsDiv, controlsDiv, stateTableDiv, workCalcDiv); controlsDiv.append(sliderContainer);// Append dynamic slider container controlsDiv.append(processExplanationDiv);// Append process explanation area// ==================== PISTON ANIMATION ====================/* PISTON-CYLINDER VISUALIZATION: This component shows a physical representation of the gas in a cylinder with a movable piston. PHYSICAL INTERPRETATION: - Cylinder walls contain the gas - Piston moves up/down based on gas volume (from ideal gas law) - External pressure (P_ext) pushes down on piston - Gas pressure pushes up on piston - At equilibrium: P_gas = P_ext (final state) VISUAL MAPPING: - Gas volume ↔ Piston height (higher piston = larger volume) - Gas color ↔ Process type (blue = expansion, orange = compression) - Arrow size ↔ External pressure magnitude */const pistonWidth =240;const pistonHeight =320;const cylinderWidth =160;const cylinderX = (pistonWidth - cylinderWidth) /2;const pistonThickness =12;// Volume range based on P_ext range (1.00 - 8.00 atm)// From ideal gas law: V = nRT/P (inverse relationship with pressure)const maxVolume = (n_moles * R_atm * T_kelvin) /1.00;// ~24.5 L at P_ext = 1.00 atm (minimum pressure)const minVolume = (n_moles * R_atm * T_kelvin) /8.00;// ~3.06 L at P_ext = 8.00 atm (maximum pressure)const pistonSvg = d3.create("svg").attr("viewBox", [0,0, pistonWidth, pistonHeight]).attr("style","width: 100%; height: auto; max-width: 240px;");// Cylinder body (fixed) - FLIPPED 180: open at top, closed at bottom, gas insideconst cylinderTop =50;const cylinderBottom =270;const cylinderHeight = cylinderBottom - cylinderTop;// Left wall pistonSvg.append("rect").attr("class","cylinder-wall-left").attr("x", cylinderX).attr("y", cylinderTop).attr("width",3).attr("height", cylinderHeight).attr("fill","#555");// Right wall pistonSvg.append("rect").attr("class","cylinder-wall-right").attr("x", cylinderX + cylinderWidth -3).attr("y", cylinderTop).attr("width",3).attr("height", cylinderHeight).attr("fill","#555");// NO TOP - cylinder is open at top// Cylinder base (bottom closed) pistonSvg.append("rect").attr("class","cylinder-base").attr("x", cylinderX).attr("y", cylinderBottom).attr("width", cylinderWidth).attr("height",5).attr("fill","#555");// Gas region (will update)const gasRect = pistonSvg.append("rect").attr("class","gas-region").attr("x", cylinderX +3).attr("width", cylinderWidth -6);// Piston (will move)const pistonGroup = pistonSvg.append("g").attr("class","piston-group");const pistonRect = pistonGroup.append("rect").attr("class","piston-head").attr("x", cylinderX +3).attr("width", cylinderWidth -6).attr("height", pistonThickness).attr("fill","#333").attr("stroke","#000").attr("stroke-width",1);// Piston rod - extends UPWARD from top of piston head (outside the cylinder, pushing down) pistonGroup.append("line").attr("class","piston-rod").attr("x1", pistonWidth /2).attr("x2", pistonWidth /2).attr("y1",0).attr("y2",-35) // Rod extending upward above piston.attr("stroke","#666").attr("stroke-width",4);// Labelsconst volumeLabel = pistonSvg.append("text").attr("class","volume-label").attr("x", pistonWidth /2).attr("text-anchor","middle").attr("font-size","14px").attr("font-weight","bold").attr("fill","#000");// Arrow for P_ext - above cylinderconst arrowGroup = pistonSvg.append("g").attr("class","pressure-arrow"); arrowGroup.append("line").attr("x1", pistonWidth /2-40).attr("x2", pistonWidth /2-40).attr("y1",10).attr("y2",40).attr("stroke","#d00").attr("stroke-width",2).attr("marker-end","url(#arrowhead)");// P_ext label - positioned to the right of arrow, vertically centeredconst pressureLabel = pistonSvg.append("text").attr("class","pressure-label").attr("x", pistonWidth /2-25).attr("y",30).attr("text-anchor","start").attr("font-size","12px").attr("font-style","italic").attr("fill","#555"); pistonSvg.append("defs").append("marker").attr("id","arrowhead").attr("markerWidth",10).attr("markerHeight",10).attr("refX",5).attr("refY",3).attr("orient","auto").append("polygon").attr("points","0 0, 10 3, 0 6").attr("fill","#d00"); pistonDiv.append(pistonSvg.node());// ==================== SPRING ANIMATION ====================/* SPRING-BLOCK VISUALIZATION: This component shows a mechanical analogy to the gas system. PHYSICAL INTERPRETATION: - Wall represents fixed boundary (like cylinder walls) - Spring can compress/extend (like gas volume changes) - Block represents the movable boundary (like piston) - External force compresses/extends spring (like external pressure) - PHYSICAL CONSTRAINTS: Spring has limited extension for visualization VISUAL CONSTRAINTS: - Maximum extension: 0.36 m beyond rest position (visualization limit) - When external force < ~35N, spring would extend beyond this limit - System constrains the block at maximum extension for visualization - Actual force differs from external force when constraints are active VISUAL MAPPING: - Spring compression ↔ Gas compression - Spring extension ↔ Gas expansion - Force magnitude ↔ External pressure - Block position ↔ Gas volume */const springWidth =240;const springHeight =320;const springWallX =50;const springBlockWidth =40;const springBlockHeight =60;// PHYSICAL CONSTRAINTS: Optimize visual scaling for better compression/extension// Maximum compression: block should compress spring significantly at F_ext = 100 Nconst springMinBlockX = springWallX +15;// Small gap from wall for visual clarity// Maximum extension: use full available space for F_ext = 0 Nconst springMaxBlockX = springWidth -25;// Keep 25px from right edge// Visual rest position for spring (when F_ext = 0 N, spring at natural length)const springRestX = springWallX +100;// Positioned to allow more extension space// Calculate visual range and scalingconst visualRange = springMaxBlockX - springMinBlockX;// Available space for block movementconst maxCompressionDistance =100/ k_spring_local;// 0.4 m for F_ext = 100 Nconst maxExtensionDistance =0.5;// Allow 0.5 m extension for F_ext = 0 Nconst totalPhysicalRange = maxCompressionDistance + maxExtensionDistance;// 0.9 m total// Dynamic scaling to fit the full physical range in available visual spaceconst visualToPhysicalScale = totalPhysicalRange / visualRange;// m per pixel// Calculate rest position relative to physical constraints// At F_ext = 50 N (initial): x = x_rest - F/k = 1.0 - 50/250 = 0.8 m// We want this to correspond to the visual rest positionconst springSvg = d3.create("svg").attr("viewBox", [0,0, springWidth, springHeight]).attr("style","width: 100%; height: auto; max-width: 240px;");// Wall (fixed) springSvg.append("rect").attr("class","wall").attr("x", springWallX -5).attr("y",100).attr("width",5).attr("height",120).attr("fill","#555");// Spring (will update) - improved zigzag with variable coilsconst springPath = springSvg.append("path").attr("class","spring-path").attr("stroke","#666").attr("stroke-width",3).attr("stroke-linecap","round").attr("stroke-linejoin","round").attr("fill","none");// Calculate initial block position based on initial force (50 N)// x = x_rest - F/k = 1.0 - 50/250 = 0.8 m// This should be positioned relative to our visual scalingconst initialBlockX = springMinBlockX + (maxExtensionDistance - (F_initial_local / k_spring_local)) / visualToPhysicalScale;// Block (will move with constraints)const springBlock = springSvg.append("rect").attr("class","spring-block").attr("x", initialBlockX).attr("y",130).attr("width", springBlockWidth).attr("height", springBlockHeight).attr("fill","#8b4513").attr("stroke","#000").attr("stroke-width",1);// Initial position indicator (blue dashed line) - positioned at spring connection point (left edge of block)const initialPositionIndicator = springSvg.append("line").attr("class","initial-position-indicator").attr("x1", initialBlockX).attr("y1",115).attr("x2", initialBlockX).attr("y2",200).attr("stroke","#0066cc").attr("stroke-width",2).attr("stroke-dasharray","5,5").attr("opacity",0.8);// Initial position labelconst initialPositionLabel = springSvg.append("text").attr("class","initial-position-label").attr("x", initialBlockX).attr("y",108).attr("text-anchor","middle").attr("font-size","12px").attr("font-weight","bold").attr("fill","#0066cc").html("<tspan style='font-style: italic;'>x</tspan>₁");// Final position indicator (red dashed line) - initially at same position as initial (spring connection point)const finalPositionIndicator = springSvg.append("line").attr("class","final-position-indicator").attr("x1", initialBlockX).attr("y1",115).attr("x2", initialBlockX).attr("y2",200).attr("stroke","#cc0000").attr("stroke-width",2).attr("stroke-dasharray","5,5").attr("opacity",0.8);// Final position labelconst finalPositionLabel = springSvg.append("text").attr("class","final-position-label").attr("x", initialBlockX).attr("y",108).attr("text-anchor","middle").attr("font-size","12px").attr("font-weight","bold").attr("fill","#cc0000").html("<tspan style='font-style: italic;'>x</tspan>₂");// Force arrow (will update) - pointing LEFT to indicate compression forceconst springArrowGroup = springSvg.append("g").attr("class","force-arrow"); springArrowGroup.append("line").attr("x1", initialBlockX + springBlockWidth +40).attr("x2", initialBlockX + springBlockWidth +10).attr("y1",160).attr("y2",160).attr("stroke","#d00").attr("stroke-width",2).attr("marker-end","url(#spring-arrowhead)");// Force label - centered over the brown weight/block, positioned above x₁ and x₂ labelsconst springForceLabel = springSvg.append("text").attr("class","force-label").attr("x", initialBlockX + springBlockWidth/2).attr("y",85).attr("text-anchor","middle").attr("font-size","11px").attr("font-style","italic").attr("fill","#555");// Displacement labelconst springDispLabel = springSvg.append("text").attr("class","disp-label").attr("x", springWidth /2).attr("y",250).attr("text-anchor","middle").attr("font-size","14px").attr("font-weight","bold").attr("fill","#000");// Displacement change label (Δx)const springDeltaXLabel = springSvg.append("text").attr("class","delta-x-label").attr("x", springWidth /2).attr("y",270).attr("text-anchor","middle").attr("font-size","13px").attr("font-weight","normal").attr("fill","#555").html('Δx = 0.000 m');// Initial position indicator (vertical line)const initialPositionLine = springSvg.append("line").attr("class","initial-position-line").attr("x1", initialBlockX).attr("x2", initialBlockX).attr("y1",120).attr("y2",200).attr("stroke","#0066cc").attr("stroke-width",2).attr("stroke-dasharray","5,5").attr("opacity",0.7);// Spring arrowhead springSvg.append("defs").append("marker").attr("id","spring-arrowhead").attr("markerWidth",10).attr("markerHeight",10).attr("refX",5).attr("refY",3).attr("orient","auto").append("polygon").attr("points","0 0, 10 3, 0 6").attr("fill","#d00");// Helper function to draw a realistic spring with smooth coilsfunctiondrawSpringPath(startX, endX, y) {const length = endX - startX;const restLength = springRestX - springWallX;const compressionRatio = length / restLength;// Ensure minimum length to prevent overlapif (length <20) {// Spring is fully compressed - draw as straight line with small bendsconst bendHeight =Math.min(6, length /3);let path =`M ${startX}${y}`;const numBends =Math.max(2,Math.floor(length /8));for (let i =0; i <= numBends; i++) {const x = startX + (i * length) / numBends;const yOffset = (i %2===0) ?0: ((i %4===1) ?-bendHeight : bendHeight); path +=` L ${x}${y + yOffset}`; }// Ensure final point is exactly at endX, y path +=` L ${endX}${y}`;return path; }// Calculate spring parameters - FIXED number of coils for consistent appearancelet amplitude;// Keep coil count consistent to maintain spring appearanceconst numCoils =8;// Adjust amplitude based on compressionif (compressionRatio <0.6) {// Compressed - smaller amplitude amplitude =10+ (compressionRatio *8); } elseif (compressionRatio <=1.2) {// Normal range amplitude =18; } else {// Extended - slightly larger amplitude amplitude =18+ (compressionRatio -1.2) *5; }// Ensure reasonable boundsconst finalAmplitude =Math.max(8,Math.min(25, amplitude));// Create smooth spring path with consistent coilsconst pointsPerCoil =8;// More points for smoother curvesconst totalPoints = numCoils * pointsPerCoil;let path =`M ${startX}${y}`;// Generate smooth spring using sine wave, ensuring we end exactly at (endX, y)for (let i =0; i <= totalPoints; i++) {const x = startX + (i * length) / totalPoints;const angle = (i / pointsPerCoil) *2*Math.PI;const yOffset =Math.sin(angle) * finalAmplitude;// The last point must be exactly at endX, y to connect to the blockif (i === totalPoints) { path +=` L ${endX}${y}`; } else {if (i ===0) { path =`M ${x}${y + yOffset}`; } else { path +=` L ${x}${y + yOffset}`; } } }return path; }// Initialize the spring path with the initial positionconst initialSpringPath =drawSpringPath(springWallX, initialBlockX,160); springPath.attr("d", initialSpringPath); springDiv.append(springSvg.node());// ==================== P-V DIAGRAM ====================/* PRESSURE-VOLUME DIAGRAM: This is the standard thermodynamic diagram for visualizing work processes. KEY FEATURES: - X-axis: Volume (V) in liters - Y-axis: Pressure (P) in atmospheres - Blue point: Initial state (P₁, V₁) from ideal gas law - Red point: Final state (P₂, V₂) after equilibration with P_ext - Horizontal dashed line: Constant external pressure (P_ext) - Shaded rectangle: Work done = P_ext × ΔV (area under the process path) - Vertical arrow: Pressure jump from P₁ to P_ext (irreversible change) THERMODYNAMIC SIGNIFICANCE: - Work = Area under process curve on P-V diagram - For constant P_ext: rectangular area (easy to calculate) - For reversible process: curved area (requires integration) - The diagram visually shows why irreversible work is simpler to calculate */const pvWidth =400;const pvHeight =300;const pvMargin = {top:20,right:20,bottom:50,left:60};const pvInnerWidth = pvWidth - pvMargin.left- pvMargin.right;const pvInnerHeight = pvHeight - pvMargin.top- pvMargin.bottom;const pvSvg = d3.create("svg").attr("viewBox", [0,0, pvWidth, pvHeight]).attr("style","width: 100%; height: auto; max-width: 400px;");const pvG = pvSvg.append("g").attr("transform",`translate(${pvMargin.left},${pvMargin.top})`);// Scalesconst xScale = d3.scaleLinear().domain([0, maxVolume *1.1]).range([0, pvInnerWidth]);const yScale = d3.scaleLinear().domain([0,11]).range([pvInnerHeight,0]);// Axesconst xAxis = d3.axisBottom(xScale).ticks(6);const yAxis = d3.axisLeft(yScale).ticks(6); pvG.append("g").attr("class","x-axis").attr("transform",`translate(0,${pvInnerHeight})`).call(xAxis); pvG.append("g").attr("class","y-axis").call(yAxis);// Axis labels pvG.append("text").attr("class","x-axis-label").attr("x", pvInnerWidth /2).attr("y", pvInnerHeight +40).attr("text-anchor","middle").attr("font-size","14px").attr("font-family","-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif").text("Volume (L)"); pvG.append("text").attr("class","y-axis-label").attr("transform","rotate(-90)").attr("x",-pvInnerHeight /2).attr("y",-45).attr("text-anchor","middle").attr("font-size","14px").attr("font-family","-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif").text("Pressure (atm)");// Work area rectangleconst workRect = pvG.append("rect").attr("class","work-area").attr("opacity",0.3);// Path for P_ext lineconst pExtLine = pvG.append("line").attr("class","p-ext-line").attr("stroke","#000").attr("stroke-width",2).attr("stroke-dasharray","5,5");// Initial state pointconst initialPoint = pvG.append("circle").attr("class","initial-point").attr("r",5).attr("fill","#00f").attr("stroke","#000").attr("stroke-width",1); pvG.append("text").attr("class","initial-label").attr("font-size","12px").attr("fill","#00f").text("Initial");// Final state pointconst finalPoint = pvG.append("circle").attr("class","final-point").attr("r",5).attr("fill","#f00").attr("stroke","#000").attr("stroke-width",1);const finalLabel = pvG.append("text").attr("class","final-label").attr("font-size","12px").attr("fill","#f00").text("Final");// Vertical arrow from initial point to P_ext line - smaller arrowhead pvSvg.append("defs").append("marker").attr("id","pressure-change-arrow").attr("markerWidth",4).attr("markerHeight",4).attr("refX",2).attr("refY",2).attr("orient","auto").append("polygon").attr("points","0 0, 4 2, 0 4").attr("fill","#666");const pressureChangeArrow = pvG.append("line").attr("class","pressure-change-arrow").attr("stroke","#666").attr("stroke-width",2).attr("marker-end","url(#pressure-change-arrow)"); pvDiagramDiv.append(pvSvg.node());// ==================== CONTROLS ====================// Dynamic control rendering functionfunctionrenderControls() { sliderContainer.innerHTML='';if (state.mode==='gas') {// Gas mode controlsconst sliderLabel = htl.html`<label style="display: block; margin-bottom: 8px; font-weight: bold;"> External Pressure (<i>P</i><sub>ext</sub>): <span id="p-ext-value">${state.P_ext.toFixed(2)}</span> atm </label>`;const slider = htl.html`<input type="range" id="gas-slider" min="1.00" max="8.00" step="0.01" value="${state.P_ext}" style="width: 100%; max-width: 500px;" class="pressure-slider">`;const resetButton = htl.html`<button class="reset-button" style="margin-left: 15px;">Reset</button>`;const controlRow = htl.html`<div style="display: flex; align-items: center; gap: 8px; margin: 12px 0;">${sliderLabel}${resetButton} </div>`; controlRow.insertBefore(slider, resetButton); sliderContainer.append(controlRow);// Add event listener for gas slider slider.addEventListener('input', (e) => { state.P_ext=parseFloat(e.target.value); sliderLabel.querySelector('#p-ext-value').textContent= state.P_ext.toFixed(2);updateVisualization(state.P_ext); });// Add event listener for gas reset button resetButton.addEventListener('click', () => { state.P_ext= P_initial; slider.value= state.P_ext; sliderLabel.querySelector('#p-ext-value').textContent= state.P_ext.toFixed(2);updateVisualization(state.P_ext); }); } elseif (state.mode==='spring') {// Spring mode controls - only external force sliderconst forceSliderLabel = htl.html`<label style="display: block; margin-bottom: 8px; font-weight: bold;"> External Force (<i>F</i><sub>ext</sub>): <span id="f-ext-value">${state.F_ext.toFixed(1)}</span> N </label>`;const forceSlider = htl.html`<input type="range" id="force-slider" min="0" max="100" step="1" value="${state.F_ext}" style="width: 100%; max-width: 500px;" class="pressure-slider">`;const resetButton = htl.html`<button class="reset-button" style="margin-left: 15px;">Reset</button>`;const controlRow = htl.html`<div style="display: flex; align-items: center; gap: 8px; margin: 12px 0;">${forceSliderLabel}${resetButton} </div>`; controlRow.insertBefore(forceSlider, resetButton); sliderContainer.append(controlRow);// Add event listener for force slider forceSlider.addEventListener('input', (e) => { state.F_ext=parseFloat(e.target.value); forceSliderLabel.querySelector('#f-ext-value').textContent= state.F_ext.toFixed(1);updateVisualization(state.F_ext); });// Add event listener for spring reset button resetButton.addEventListener('click', () => { state.F_ext= F_initial_local; forceSlider.value= state.F_ext; forceSliderLabel.querySelector('#f-ext-value').textContent= state.F_ext.toFixed(1);updateVisualization(state.F_ext); }); } }// ==================== WIDGET TITLE UPDATE ====================functionupdateWidgetTitle() {if (state.mode==='gas') { titleDiv.innerHTML=` <div class="widget-title"> Irreversible Work: Piston-Cylinder System <p class="widget-subtitle">Isothermal Ideal Gas Process (T = ${T_kelvin} K, n = ${n_moles} mol)</p> </div> `; } elseif (state.mode==='spring') { titleDiv.innerHTML=` <div class="widget-title"> Irreversible Work: Spring-Block System <p class="widget-subtitle">Hooke's Law Process (k = ${k_spring_local} N/m, F<sub>initial</sub> = ${F_initial_local} N)</p> </div> `; } }// ==================== PROCESS EXPLANATION UPDATE ====================functionupdateProcessExplanation(controlValue, calc) {let explanation;if (state.mode==='gas') {const work = calc.work_kJ;// Check the sign of work and select appropriate explanationif (work <0) { explanation = processExplanations.gas.negative; } elseif (work >0) { explanation = processExplanations.gas.positive; } else {// Zero work - sophisticated logic to determine cause explanation = { ...processExplanations.gas.zero };// Copy object to allow modification// Check if zero work is due to ΔV = 0 or P_ext = 0if (Math.abs(calc.deltaV) <1e-10) {// ΔV = 0 case - volumes are the same explanation.blurb="No work is performed. This occurs because the initial and final volumes are the same (<b>Δ<i>V</i> = 0</b>). Since there is no volume change, no work was done regardless of the pressure difference."; } elseif (Math.abs(controlValue) <1e-10) {// P_ext = 0 case - vacuum expansion explanation.blurb="No work is performed. This occurs because the gas is expanding into a vacuum (<b>P<sub>ext</sub> = 0</b>). Although the gas expands, there is no opposing pressure to work against, so no work is done."; }// Otherwise, use the default blurb which covers both cases } } elseif (state.mode==='spring') {const work = calc.work_J;// Check the sign of work and select appropriate explanationif (work <0) { explanation = processExplanations.spring.negative; } elseif (work >0) { explanation = processExplanations.spring.positive; } else {// Zero work - sophisticated logic to determine cause explanation = { ...processExplanations.spring.zero };// Copy object to allow modification// Check if zero work is due to Δx = 0 or F_ext = 0if (Math.abs(calc.deltaX) <1e-10) {// Δx = 0 case - positions are the same explanation.blurb="No work is performed. This occurs because the initial and final positions of the block are the same (<b>Δ<i>x</i> = 0</b>). Since there is no displacement, no work was done regardless of the force applied."; } elseif (Math.abs(controlValue) <1e-10) {// F_ext = 0 case - free expansion explanation.blurb="This is a free expansion to the spring's natural equilibrium (rest length); no work is done as there is no opposing force."; }// Otherwise, use the default blurb which covers both cases } }// No constraint warning needed - visual indicators (red spring, dual force display) are sufficientlet constraintWarning ='';// Update the process explanation display processExplanationDiv.innerHTML=` <div class="process-explanation-title">${explanation.title}</div> <div class="process-explanation-content">${explanation.blurb}</div>${constraintWarning} `; }// ==================== UPDATE FUNCTION ====================functionupdateVisualization(controlValue) {const calc =calculateState(controlValue);if (state.mode==='gas') {// Show gas visualization, hide spring pistonDiv.style.display='block'; springDiv.style.display='none';const P_ext = controlValue;// Update piston position - Map volume to visual position with constraintsconst volumeFraction = (calc.V_final- minVolume) / (maxVolume - minVolume);const usableHeight = cylinderHeight *0.85;const pistonY = cylinderTop + (1- volumeFraction) * usableHeight; pistonGroup.transition().duration(200).attr("transform",`translate(0, ${pistonY})`);// Update gas region gasRect.transition().duration(200).attr("y", pistonY + pistonThickness).attr("height", cylinderBottom - pistonY - pistonThickness).attr("fill", calc.isExpansion?"rgba(100, 200, 255, 0.5)": calc.isCompression?"rgba(255, 150, 100, 0.5)":"rgba(150, 150, 255, 0.5)");// Update labels volumeLabel.attr("y", (pistonY + pistonThickness + cylinderBottom) /2+5).html(`<tspan style="font-style: italic;">V</tspan> = ${roundBankers(calc.V_final, calc.sigFigs).toPrecision(calc.sigFigs)} L`); pressureLabel.html(`<tspan style="font-style: italic;">P</tspan><tspan baseline-shift="sub" font-size="9px">ext</tspan> = ${P_ext.toFixed(2)} atm`);// Update P-V diagram axes and labels for gas mode pvG.select(".x-axis-label").text("Volume (L)").attr("font-family","-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"); pvG.select(".y-axis-label").text("Pressure (atm)").attr("font-family","-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif");// Update P-V diagram initialPoint.attr("cx",xScale(V_initial)).attr("cy",yScale(P_initial)); pvG.select(".initial-label").attr("x",xScale(V_initial) +10).attr("y",yScale(P_initial) -10); finalPoint.transition().duration(200).attr("cx",xScale(calc.V_final)).attr("cy",yScale(calc.P_final)); finalLabel.transition().duration(200).attr("x",xScale(calc.V_final) +10).attr("y",yScale(calc.P_final) -10);// Update P_ext line// Hide line if initial and final volumes are equalconst showPextLine =Math.abs(calc.deltaV) >1e-10; pExtLine.transition().duration(200).attr("x1",xScale(Math.min(V_initial, calc.V_final))).attr("y1",yScale(P_ext)).attr("x2",xScale(Math.max(V_initial, calc.V_final))).attr("y2",yScale(P_ext)).style("opacity", showPextLine ?1:0);// Update vertical arrow showing pressure change// Only show arrowhead and line if initial and final states differconst showArrowhead =Math.abs(P_ext - P_initial) >1e-10; pressureChangeArrow.transition().duration(200).attr("x1",xScale(V_initial)).attr("y1",yScale(P_initial)).attr("x2",xScale(V_initial)).attr("y2",yScale(P_ext) +5).attr("marker-end", showArrowhead ?"url(#pressure-change-arrow)":"none").style("opacity", showArrowhead ?1:0);// Update work rectangleconst rectX =Math.min(V_initial, calc.V_final);const rectWidth =Math.abs(calc.deltaV); workRect.transition().duration(200).attr("x",xScale(rectX)).attr("y",yScale(P_ext)).attr("width",xScale(rectX + rectWidth) -xScale(rectX)).attr("height",yScale(0) -yScale(P_ext)).attr("fill", calc.isExpansion?"#888":"#d00"); } elseif (state.mode==='spring') {// Show spring visualization, hide gas pistonDiv.style.display='none'; springDiv.style.display='block';const F_ext = controlValue;// PHYSICAL CONSTRAINTS: Calculate constrained block position// Calculate theoretical position from Hooke's law: F = k(x_rest - x)const k_fixed = k_spring_local;// Use fixed spring constant// Physical position: x = x_rest - F/kconst physicalPosition = x_rest_local - (F_ext / k_fixed);// Convert to visual coordinates, accounting for our extended range// Position measured from left edge (minimum compression)const theoreticalX = springMinBlockX + (maxExtensionDistance - (F_ext / k_fixed)) / visualToPhysicalScale;// Apply visual constraints - keep within animation windowconst maxBlockRight = springWidth -25;// Keep 25px from right edgeconst maxBlockX = maxBlockRight - springBlockWidth;const minBlockX = springMinBlockX;// Minimum position from wallconst constrainedX =Math.max(minBlockX,Math.min(maxBlockX, theoreticalX));// Hide block when F_ext = 0, otherwise show and move itif (F_ext ===0) { springBlock.style("opacity",0); } else { springBlock.style("opacity",1).transition().duration(200).attr("x", constrainedX); }// Update final position indicator to always show current position (spring connection point = left edge of block) finalPositionIndicator.transition().duration(200).attr("x1", constrainedX).attr("x2", constrainedX); finalPositionLabel.transition().duration(200).attr("x", constrainedX);// Update spring path with realistic compression/extension// Ensure spring connects exactly to the left edge of the block (same as x2 indicator)const springPathData =drawSpringPath(springWallX, constrainedX,160);// Update spring path immediately (SVG path transitions can be problematic between different paths) springPath.attr("d", springPathData);// Update spring stroke width based on compressionconst compressionRatio = (constrainedX - springWallX) / (springRestX - springWallX);let strokeWidth =3;if (compressionRatio <0.5) { strokeWidth =4;// Thicker when compressed } elseif (compressionRatio >1.3) { strokeWidth =2;// Thinner when extended }// Update spring color based on compression/extension stateconst isActuallyCompressed = F_ext > F_initial_local;// Higher external force = compressionconst isActuallyExtended = F_ext < F_initial_local;// Lower external force = extensionlet springColor = isActuallyCompressed ?"#ff8c00": isActuallyExtended ?"#4169e1":"#666";// Warn user if constraint is activeif (constrainedX !== theoreticalX) { springColor ="#ff0000";// Red color to indicate constraint strokeWidth =4;// Make it thicker when hitting constraint } springPath.attr("stroke", springColor).attr("stroke-width", strokeWidth);// Calculate actual force that would produce constrained positionconst actualCompression = maxExtensionDistance - (constrainedX - springMinBlockX) * visualToPhysicalScale;const actualF_ext =Math.round(k_fixed * actualCompression *100) /100;// Round to 2 decimal places to avoid floating point errors// Hide arrow and block when force is zero (free expansion)const displayForce = constrainedX !== theoreticalX ? actualF_ext : F_ext;const arrowLength = displayForce >0?Math.min(40, displayForce /4) :0; springArrowGroup.select("line").transition().duration(200).attr("x1", displayForce >0? constrainedX + springBlockWidth +10+ arrowLength : constrainedX + springBlockWidth +10).attr("x2", constrainedX + springBlockWidth +10).style("opacity", displayForce >0?1:0);// Update labels with actual values - center over blockconst forceDisplayText = constrainedX !== theoreticalX ?`${F_ext.toFixed(1)} N (${actualF_ext.toFixed(1)} N actual)`:`${F_ext.toFixed(1)} N`; springForceLabel.html(`<tspan style="font-style: italic;">F</tspan><tspan baseline-shift="sub" font-size="9px">ext</tspan> = ${forceDisplayText}`); springForceLabel.transition().duration(200).attr("x", constrainedX + springBlockWidth/2) // Center over block.attr("y",85) // Keep vertical position constant, positioned above x₁ and x₂ labels.style("opacity", F_ext >0?1:0.3);// Fade label when force is zero// Calculate actual displacement for displayconst actualX_final = x_rest_local - (actualF_ext / k_fixed); springDispLabel.html(`<tspan style="font-style: italic;">x</tspan> = ${formatNumberForHTML(actualX_final, calc.sigFigs)} m`);// Update Δx display with proper Unicode Delta character and HTML minus signs springDeltaXLabel.html(`Δ<tspan style="font-style: italic;">x</tspan> = ${formatNumberForHTML(calc.deltaX, calc.sigFigs)} m`);// Show/hide position indicators based on forceconst showIndicators = F_ext >0; initialPositionLine.style("opacity", showIndicators ?0.7:0.3); initialPositionLabel.style("opacity", showIndicators ?1:0.3); finalPositionIndicator.style("opacity", showIndicators ?0.7:0.3); finalPositionLabel.style("opacity", showIndicators ?1:0.3); springDeltaXLabel.style("opacity", showIndicators ?1:0.3);// Update diagram for spring mode (F-x diagram) pvG.select(".x-axis-label").text("Displacement (m)").attr("font-family","-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"); pvG.select(".y-axis-label").text("Force (N)").attr("font-family","-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif");// Update scales for spring modeconst xScaleSpring = d3.scaleLinear().domain([0.35,1.05]) // Expanded range to accommodate enhanced compression.range([0, pvInnerWidth]);const yScaleSpring = d3.scaleLinear().domain([0,120]) // Increased to 120 N to handle 0-100 N range comfortably.range([pvInnerHeight,0]);// Update axes for spring modeconst xAxisSpring = d3.axisBottom(xScaleSpring).ticks(6);const yAxisSpring = d3.axisLeft(yScaleSpring).ticks(6); pvG.select(".x-axis").call(xAxisSpring); pvG.select(".y-axis").call(yAxisSpring);// Update points for spring mode with actual constrained values initialPoint.attr("cx",xScaleSpring(x_initial_local)).attr("cy",yScaleSpring(F_initial_local)); pvG.select(".initial-label").attr("x",xScaleSpring(x_initial_local) +10).attr("y",yScaleSpring(F_initial_local) -10); finalPoint.transition().duration(200).attr("cx",xScaleSpring(actualX_final)).attr("cy",yScaleSpring(actualF_ext)); finalLabel.transition().duration(200).attr("x",xScaleSpring(actualX_final) +10).attr("y",yScaleSpring(actualF_ext) -10);// Update F_ext line with actual force// Hide line if initial and final positions are equalconst showFextLine =Math.abs(calc.deltaX) >1e-10; pExtLine.transition().duration(200).attr("x1",xScaleSpring(Math.min(x_initial_local, actualX_final))).attr("y1",yScaleSpring(actualF_ext)).attr("x2",xScaleSpring(Math.max(x_initial_local, actualX_final))).attr("y2",yScaleSpring(actualF_ext)).style("opacity", showFextLine ?1:0);// Update vertical arrow showing force change// Only show arrowhead and line if initial and final states differconst showArrowheadSpring =Math.abs(F_ext - F_initial_local) >1e-10; pressureChangeArrow.transition().duration(200).attr("x1",xScaleSpring(x_initial_local)).attr("y1",yScaleSpring(F_initial_local)).attr("x2",xScaleSpring(x_initial_local)).attr("y2",yScaleSpring(F_ext) +5).attr("marker-end", showArrowheadSpring ?"url(#pressure-change-arrow)":"none").style("opacity", showArrowheadSpring ?1:0);// Update work rectangle for spring mode with actual constrained valuesconst actualDeltaX = actualX_final - x_initial_local;const rectX =Math.min(x_initial_local, actualX_final);const rectWidth =Math.abs(actualDeltaX); workRect.transition().duration(200).attr("x",xScaleSpring(rectX)).attr("y",yScaleSpring(actualF_ext)).attr("width",xScaleSpring(rectX + rectWidth) -xScaleSpring(rectX)).attr("height",yScaleSpring(0) -yScaleSpring(actualF_ext)).attr("fill", calc.isExpansion?"#888":"#d00"); }// Update state tableupdateStateTable(calc);// Update work calculationupdateWorkCalculation(controlValue, calc);// Update process explanationupdateProcessExplanation(controlValue, calc); }// ==================== STATE TABLE ====================functionupdateStateTable(calc) {let tableHTML;if (state.mode==='gas') { tableHTML =` <table class="results-table" style="margin: 12px auto; max-width: 600px;"> <thead style="background: #34495e;"> <tr> <th style="color: white; background: #34495e;">State</th> <th style="color: white; background: #34495e;"><i>P</i><sub>ext</sub> (atm)</th> <th style="color: white; background: #34495e;"><i>V</i> (L)</th> </tr> </thead> <tbody> <tr> <td><strong>Initial</strong></td> <td>${formatNumberForHTML(P_initial,3)}</td> <td>${formatNumberForHTML(V_initial,3)}</td> </tr> <tr> <td><strong>Final</strong></td> <td>${formatNumberForHTML(calc.P_final, calc.sigFigs)}</td> <td>${formatNumberForHTML(calc.V_final, calc.sigFigs)}</td> </tr> <tr> <td><strong>Change</strong></td> <td>${formatNumberForHTML(calc.P_final- P_initial, calc.sigFigs)}</td> <td>${formatNumberForHTML(calc.deltaV, calc.sigFigs)}</td> </tr> </tbody> </table> `; } else { tableHTML =` <table class="results-table" style="margin: 12px auto; max-width: 600px;"> <thead style="background: #34495e;"> <tr> <th style="color: white; background: #34495e;">State</th> <th style="color: white; background: #34495e;"><i>F</i><sub>ext</sub> (N)</th> <th style="color: white; background: #34495e;"><i>x</i> (m)</th> </tr> </thead> <tbody> <tr> <td><strong>Initial</strong></td> <td>${formatNumberForHTML(F_initial_local,3)}</td> <td>${formatNumberForHTML(x_initial_local,3)}</td> </tr> <tr> <td><strong>Final</strong></td> <td>${formatNumberForHTML(calc.F_final, calc.sigFigs)}</td> <td>${formatNumberForHTML(calc.x_final, calc.sigFigs)}</td> </tr> <tr> <td><strong>Change</strong></td> <td>${formatNumberForHTML(Math.abs(calc.F_final- F_initial_local) <1e-10?0: calc.F_final- F_initial_local, calc.sigFigs)}</td> <td>${formatNumberForHTML(calc.deltaX, calc.sigFigs)}</td> </tr> </tbody> </table> `; } stateTableDiv.innerHTML= tableHTML; }// ==================== WORK CALCULATION ====================// Helper function to build LaTeX without Quarto interferencefunctionbuildLatexString(lines) {const mathDelim ='$'+'$';const bs =String.fromCharCode(92);// Backslash character// Build the align* environment properlyconst alignStart = bs +'begin{align*}';const alignEnd = bs +'end{align*}';// Create line breaks: \\ followed by [1.5ex]const lineBreak = bs + bs +'[1.5ex]';const lineBreak2ex = bs + bs +'[2ex]';// Process each line and add line breaksconst processedLines = lines.map((line, idx) => {if (idx === lines.length-1) {// Last line - no line breakreturn line; }// Check for special 2ex break markerif (line.includes('BREAK2EX')) {return line.replace('BREAK2EX','') +' '+ lineBreak2ex; }// Normal line breakreturn line +' '+ lineBreak; });// Join all lines with proper spacingconst latexContent = processedLines.join('\n ');// Build complete LaTeX stringreturn mathDelim +' '+ alignStart +'\n '+ latexContent +'\n '+ alignEnd +' '+ mathDelim; }functionupdateWorkCalculation(controlValue, calc) {let latexCalc;if (state.mode==='gas') {const P_ext = controlValue;/* WORK CALCULATION METHODOLOGY - GAS MODE: The work equation w = -P_ext × ΔV derives from the definition of work: w = -∫P_ext dV, where P_ext is constant for irreversible processes Step-by-step calculation shown: 1. Start with fundamental equation: w = -P_ext × ΔV 2. Substitute volume values: w = -P_ext × (V_final - V_initial) 3. Insert numerical values with units 4. Calculate volume change 5. Multiply by external pressure (work in L·atm) 6. Apply significant figure rounding 7. Convert to kJ using exact conversion factor (1 L·atm = 0.101325 kJ) 8. Round to appropriate significant figures PHYSICAL INTERPRETATION: - Negative work: System does work on surroundings (expansion) - Positive work: Surroundings do work on system (compression) - Zero work: No volume change */const processType = calc.isExpansion?"Expansion (work done <b>by</b> system)": calc.isCompression?"Compression (work done <b>on</b> system)":"No net change";const signNote = calc.work_kJ<0?"negative (the system loses energy)": calc.work_kJ>0?"positive (the system gains energy)":"zero";// Build LaTeX lines programmatically to avoid Quarto escaping issuesconst bs =String.fromCharCode(92);// backslashconst latexLines = [`w &= -P_{${bs}text{ext}}~${bs}Delta V`,`&= -P_{${bs}text{ext}}~(V_2 - V_1)`,`&= -(${getInputWithBar(P_ext, calc.sigFigs)}~${bs}mathrm{atm})(${getInputWithBar(calc.V_final, calc.sigFigs)} - ${getInputWithBar(V_initial,3)})~${bs}mathrm{L}`,`&= -(${getInputWithBar(P_ext, calc.sigFigs)}~${bs}mathrm{atm})(${getInputWithBar(calc.deltaV, calc.sigFigs, calc.deltaV_decimalPlaces)}~${bs}mathrm{L})`,`&= ${getUnroundedWithBar(calc.work_Latm, calc.sigFigs, calc.work_Latm_decimalPlaces)}~${bs}mathrm{L~atm}`,`&= ${roundBankers(calc.work_Latm, calc.sigFigs).toPrecision(calc.sigFigs)}~${bs}mathrm{L~atm}BREAK2EX`,`&= (${getUnroundedWithBar(calc.work_Latm, calc.sigFigs, calc.work_Latm_decimalPlaces)}~${bs}mathrm{L~atm})${bs}left( ${bs}frac{0.101325~${bs}mathrm{kJ}}{1~${bs}mathrm{L~atm}} ${bs}right)`,`&= ${getUnroundedWithBar(calc.work_kJ, calc.sigFigs, calc.work_kJ_decimalPlaces)}~${bs}mathrm{kJ}`,`&= ${roundBankers(calc.work_kJ, calc.sigFigs).toPrecision(calc.sigFigs)}~${bs}mathrm{kJ}` ];const latexMath =buildLatexString(latexLines);// Debug: log the generated LaTeXconsole.log('Generated LaTeX for gas mode:', latexMath); latexCalc =` <div style="max-width: 800px; margin: 12px auto;"> <div class="calculation-box" style="text-align: center;">${latexMath} </div> </div> `; } else {const F_ext = controlValue;/* WORK CALCULATION METHODOLOGY - SPRING MODE: The work equation w = F_ext × Δx derives from the definition of mechanical work: w = ∫F_ext dx, where F_ext is constant for the process Step-by-step calculation shown: 1. Start with fundamental equation: w = F_ext × Δx 2. Substitute displacement values: w = F_ext × (x_final - x_initial) 3. Insert numerical values with units 4. Calculate displacement change 5. Multiply by external force (work in Joules) 6. Apply significant figure rounding 7. Convert to kJ for display consistency PHYSICAL INTERPRETATION: - Positive work: Force applied in direction of displacement (compression) - Negative work: Force applied opposite to displacement (extension) - Zero work: No displacement change */const processType = calc.isCompression?"Compression (work done <b>on</b> spring)": calc.isExpansion?"Extension (work done <b>by</b> spring)":"No net change";const signNote = calc.work_kJ>0?"positive (energy stored in spring)": calc.work_kJ<0?"negative (energy released from spring)":"zero";// Build LaTeX lines programmatically to avoid Quarto escaping issuesconst bs =String.fromCharCode(92);// backslashconst latexLines = [`w &= F_{${bs}text{ext}}~${bs}Delta x`,`&= F_{${bs}text{ext}}~(x_2 - x_1)`,`&= (${getInputWithBar(F_ext, calc.sigFigs)}~${bs}mathrm{N})~(${getInputWithBar(calc.x_final, calc.sigFigs)} - ${getInputWithBar(x_initial_local,3)})~${bs}mathrm{m}`,`&= (${getInputWithBar(F_ext, calc.sigFigs)}~${bs}mathrm{N}) ~ (${getInputWithBar(calc.deltaX, calc.sigFigs, calc.deltaX_decimalPlaces)}~${bs}mathrm{m})`,`&= ${getUnroundedWithBar(calc.work_J, calc.sigFigs, calc.work_J_decimalPlaces)}~${bs}mathrm{J}`,`&= ${roundBankers(calc.work_J, calc.sigFigs).toPrecision(calc.sigFigs)}~${bs}mathrm{J}BREAK2EX`,`&= (${getUnroundedWithBar(calc.work_J, calc.sigFigs, calc.work_J_decimalPlaces)}~${bs}mathrm{J})${bs}left(${bs}dfrac{1~${bs}mathrm{kJ}}{10^3~${bs}mathrm{J}}${bs}right)`,`&= ${getUnroundedWithBar(calc.work_kJ, calc.sigFigs, calc.work_kJ_decimalPlaces)}~${bs}mathrm{kJ}`,`&= ${roundBankers(calc.work_kJ, calc.sigFigs).toPrecision(calc.sigFigs)}~${bs}mathrm{kJ}` ];const latexMath =buildLatexString(latexLines);// Debug: log the generated LaTeXconsole.log('Generated LaTeX for spring mode:', latexMath); latexCalc =` <div style="max-width: 800px; margin: 12px auto;"> <div class="calculation-box" style="text-align: center;">${latexMath} </div> </div> `; }// Clear previous content first to prevent double-rendering workCalcDiv.innerHTML='';// Use a timeout to ensure DOM is ready and avoid race conditions with page-level MathJaxsetTimeout(() => { workCalcDiv.innerHTML= latexCalc;// Force MathJax to clear any previous typesetting in this divif (window.MathJax&&window.MathJax.typesetClear) {window.MathJax.typesetClear([workCalcDiv]); }// Trigger MathJax rendering with comprehensive style resetif (window.MathJax&&window.MathJax.typesetPromise) {window.MathJax.typesetPromise([workCalcDiv]).then(() => {// Apply comprehensive styling to override global site stylessetTimeout(() => {const widgetElement =document.getElementById('work-widget-container');if (widgetElement) {// Reset ALL MathJax elements to prevent global inheritanceconst allMathJaxElements = widgetElement.querySelectorAll('.MathJax, .MathJax_Display, mjx-container, .MathJax_CHTML, .MathJax_SVG'); allMathJaxElements.forEach(element => { element.style.setProperty('font-size','100%','important'); element.style.setProperty('line-height','normal','important'); element.style.setProperty('overflow','visible','important'); element.style.setProperty('white-space','normal','important'); element.style.setProperty('word-wrap','normal','important'); element.style.setProperty('display', element.classList.contains('MathJax_Display') ?'block':'inline-block','important'); });// Hide equation numbers in align* environmentsconst refElements = widgetElement.querySelectorAll('.MathJax_ref, .mjx-label'); refElements.forEach(ref => { ref.style.setProperty('display','none','important'); });// Ensure display equations are centeredconst mathDisplays = widgetElement.querySelectorAll('.MathJax_Display'); mathDisplays.forEach(display => { display.style.setProperty('text-align','center','important'); display.style.setProperty('margin','1.5em 0','important'); display.style.setProperty('display','block','important'); });// Target nested container elements specifically - use setProperty for !importantconst nestedContainers = widgetElement.querySelectorAll('mjx-container mjx-container, .MathJax mjx-container, .MathJax_Display mjx-container'); nestedContainers.forEach(container => { container.style.setProperty('font-size','100%','important'); container.style.setProperty('overflow','visible','important'); container.style.setProperty('white-space','normal','important'); container.style.setProperty('display','inline-block','important'); });// Special handling for mjx-container elements to override global 110%const mjxContainers = widgetElement.querySelectorAll('mjx-container'); mjxContainers.forEach(container => { container.style.setProperty('font-size','100%','important'); container.style.setProperty('overflow-x','visible','important'); container.style.setProperty('overflow-y','visible','important'); }); } },100); }).catch((err) => {console.log('MathJax rendering error:', err);// Fallback: try to render with older MathJax methodsif (window.MathJax&&window.MathJax.Hub) {window.MathJax.Hub.Queue(["Typeset",window.MathJax.Hub, workCalcDiv]); } }); } },50); }// ==================== EVENT LISTENERS ====================// Mode toggle event listeners - use querySelector on container to find buttonsconst gasModeBtn = container.querySelector('#gas-mode-btn');const springModeBtn = container.querySelector('#spring-mode-btn');if (gasModeBtn && springModeBtn) { gasModeBtn.addEventListener('click', () => { state.mode='gas'; gasModeBtn.classList.add('active'); springModeBtn.classList.remove('active');updateWidgetTitle();// Update title when mode changesrenderControls();updateVisualization(state.P_ext); }); springModeBtn.addEventListener('click', () => { state.mode='spring'; springModeBtn.classList.add('active'); gasModeBtn.classList.remove('active');updateWidgetTitle();// Update title when mode changesrenderControls();updateVisualization(state.F_ext); }); }// Initial renderupdateWidgetTitle();// Set initial titlerenderControls();updateVisualization(state.P_ext);return container;}
TipAdvanced Topic: Reversible Processes and Maximum Work
The single-step expansion we discussed (w = -P_ext * ΔV) is fast, simple, and happens against a constant external pressure. This type of process is called irreversible. But is it the most efficient way for a system to perform work?
Consider the expansion of a gas in a cylinder. To get the most work out of the expansion, we would want to push against the highest possible pressure at every step. The highest possible pressure is one that is just infinitesimally less than the gas’s own internal pressure. If we could manage this delicate balance throughout the entire expansion, we would achieve a reversible process.
The Path Matters: Reversible vs. Irreversible
A reversible process is a theoretical pathway that proceeds in an infinite number of tiny steps. At every moment, the system is perfectly in equilibrium with its surroundings.
Irreversible Expansion (One Big Step): Imagine a piston held in place by a large weight. If we suddenly remove the weight, the external pressure drops instantly to a lower constant value, and the gas expands rapidly against this new pressure. The work done is w = -P_ext * ΔV, where Pext is this final, constant external pressure.
Reversible Expansion (Infinite Tiny Steps): Now, imagine the piston is held down by a pile of very fine sand. If we remove one grain of sand at a time, the external pressure decreases by a tiny amount, and the gas expands by a tiny amount. We repeat this process an infinite number of times. At every step, the external pressure is perfectly matched to the internal pressure of the gas (P_ext ≈ P_gas).
Graphical View of Work
This difference is easiest to see on a PV graph, where the work done is the area under the curve.
The figure above presents a direct, side-by-side comparison of the work done when a gas expands from the same initial state to the same final state via two different pathways.
1. The Left Panel: Irreversible Path
This panel illustrates a sudden, single-step expansion. Imagine the gas is held at a high pressure (P1) and the external pressure is instantly dropped to the final pressure (P2). The gas then expands against this new, constant external pressure.
The path is shown by the dashed red line.
The work done, wirrev, corresponds to the shaded red rectangular area. This area is calculated as w = -P_ext * ΔV, where P_ext is the low, final pressure.
2. The Right Panel: Reversible Path
This panel shows the ideal, slow expansion where the external pressure is always perfectly matched to the gas’s internal pressure. As the gas expands, the pressure decreases gradually along the solid blue curve.
The work done, wrev, is the entire shaded blue area under the curve.
The Conclusion
By visually comparing the two panels, the conclusion is immediate and clear: the shaded area for the reversible path is significantly larger than the area for the irreversible path. This graphically demonstrates the fundamental principle of this topic:
The maximum work a system can do on its surroundings is achieved through a reversible pathway.
|wrev| > |wirrev|
The single, fast, irreversible step is less efficient at getting work done because the expansion happens against a much lower average pressure compared to the slow, controlled, reversible process.
For a gas expanding from V1 to V2:
Irreversible Work: The work is the rectangular area defined by the constant, final external pressure (P_ext) and ΔV.
Reversible Work: The work is the entire area under the curved path that the gas follows as its internal pressure drops during the expansion.
As you can see from a typical PV diagram, the area under the curve for the reversible path is significantly larger than the rectangular area for the irreversible path. This leads to a critical conclusion:
The maximum amount of work that can be done by a system during an expansion is achieved through a reversible pathway.
The Equation for Reversible Work
Because the pressure is constantly changing during a reversible expansion, we cannot use the simple -PΔV formula. Calculating the area under this curve requires calculus. For an ideal gas at a constant temperature (an isothermal reversible process), the work is given by the equation: \[
w_{\mathrm{rev}} = -nRT \ln \left( \frac{V_{\mathrm{final}}}{V_{\mathrm{initial}}} \right)
\] where
wrev is the reversible work, typically in Joules
n is the number of moles of gas
R is the ideal gas constant (≈8.314 J mol−1 K−1)
T is the absolute temperature in Kelvin
Vfinal is the final volume of the gas
Vinitial is the initial volume of the gas
This equation is fundamental for calculating the theoretical maximum work for gas expansions and is a benchmark for thermodynamic efficiency.
Chemical Reactions and Work
While the image of a piston in a cylinder is a useful model, pressure-volume work is a fundamental aspect of many common chemical reactions. For chemical reactions, the primary source of pressure-volume work is the production or consumption of gases and can be signified by the change in moles of the gas (Δngas) of the reaction.
This is because the volume occupied by one mole of gas is vastly larger than the volume of the same amount of a liquid or solid. Therefore, any change in the number of moles of gas results in a significant change in the system’s total volume, forcing the system to push against the constant pressure of the atmosphere.
If a reaction produces more moles of gas than it consumes, the system expands. If it consumes more moles of gas than it produces, the system contracts (is compressed).
The stoichiometric coefficients in a balanced equation are dimensionless ratios. When calculating the actual change in moles of gas for a reaction, we interpret these coefficients as moles. This gives Δngas units of mol, which is needed for calculating the work done (w = −PΔV) or total energy changes (ΔH, ΔU).
Here, the reaction starts with 0 moles of gas but ends with 1 mole of gas. The change in moles of gas (Δngas) is
\[
\begin{align*}
\Delta n_{\mathrm{gas}}
&= \sum n(\mathrm{products})_{\mathrm{gas}} ~ - ~ \sum n(\mathrm{reactants})_{\mathrm{gas}} \\[1.5ex]
&= \,\bigr [ n(\mathrm{CO_2(g)}) \bigl ] ~ - ~ \bigr [ 0~\mathrm{mol} \bigl] \\[1.5ex]
&= \,\bigr [ 1~\mathrm{mol} \bigl ] ~ - ~ \bigr [ 0~\mathrm{mol} \bigl ] \\[1.5ex]
&= 1~\mathrm{mol}
\end{align*}
\] Since Δngas is positive, the system expands and performs PV-work on the surroundings. This constitutes work done by the system, meaning the value of work (w) is negative.
When the change in the number of moles of gas is zero, the volume change (ΔV) for the reaction is considered negligible at constant temperature and pressure. Consequently, the pressure-volume work (w) is approximately zero.
Practice
Consider the vigorous reaction of zinc metal with hydrochloric acid, a common experiment in a general chemistry lab: \[
\mathrm{Zn(s) + 2~HCl(aq) \longrightarrow H_2(g) + ZnCl_2(aq)}
\]
Determine if work is positive, negative, or approximately zero.
Solution
The change in moles of gas is calculated from the stoichiometry of the reaction.
Since Δngas is positive, a gas is produced, causing the system to expand and perform work on the surroundings. By convention, work done by the system is negative.
Practice
The combustion of octane in a car engine is the classic example of a chemical reaction generating work. \[
\mathrm{2~C_8H_{18}(g) + 25~O_2(g) \longrightarrow 16~CO_2(g) + 18~H_2O(g)}
\]
Determine if work is positive, negative, or approximately zero.
Solution
The change in moles of gas is calculated from the stoichiometry of the reaction.
Since Δngas is positive, a gas is produced, causing the system to expand and perform work on the surroundings. By convention, work done by the system is negative.
This is the type of reaction that takes place inside the internal combustion engine in a typical car. A sudden, massive increase in the amount of gas inside the engine cylinder, combined with the heat produced, creates an enormous pressure that forcefully pushes the piston down, doing the mechanical work of moving a car.
Practice
Biological systems can perform PV-work. The metabolic breakdown of glucose is a form of combustion: \[
\mathrm{C_6H_{12}O_6(s) + 6~O_2(g) \longrightarrow 6~CO_2(g) + 6~H_2O(l)}
\]
Determine if work is positive, negative, or approximately zero.
Solution
The change in moles of gas is calculated from the stoichiometry of the reaction.
In this specific case, the number of moles of gas is the same on both sides and Δngas = 0. Therefore, the PV-work done by our bodies from this process is essentially zero. Almost all of the energy from metabolism is released as heat.
Summary
Work in Thermodynamics
Work (w) is the transfer of energy via organized, uniform motion. In chemistry, the most common form is pressure-volume work:
\[
w = -P_{\mathrm{ext}} \Delta V
\]
The negative sign ensures proper sign convention from the system’s point of view:
Expansion (ΔV > 0): System does work on surroundings, w < 0 (energy leaves)
Compression (ΔV < 0): Surroundings do work on system, w > 0 (energy enters)
Work in Chemical Reactions
For chemical reactions, PV-work arises from changes in the number of moles of gas (Δngas):
Δngas > 0: System expands, w is negative
Δngas < 0: System contracts, w is positive
Δngas = 0: No significant PV-work
The SI unit for work is the Joule (J). When using L and atm, convert using 1 L atm = 101.325 J.