guohanghui commited on
Commit
ec106dc
·
verified ·
1 Parent(s): 36ea9e6

Update PySDM/mcp_output/mcp_plugin/mcp_service.py

Browse files
PySDM/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -10,7 +10,12 @@ if source_path not in sys.path:
10
 
11
  from fastmcp import FastMCP
12
  import numpy as np
13
- from scipy import constants as sci
 
 
 
 
 
14
 
15
  # Create the FastMCP service application
16
  mcp = FastMCP("pysdm_service")
@@ -18,50 +23,6 @@ mcp = FastMCP("pysdm_service")
18
 
19
  # ===================== Physical Constants =====================
20
 
21
- # Define key physical constants (based on PySDM's constants_defaults.py)
22
- PHYSICAL_CONSTANTS = {
23
- "R_str": sci.R, # Universal gas constant (J/K/mol)
24
- "N_A": sci.N_A, # Avogadro constant (1/mol)
25
- "T0": sci.zero_Celsius, # 0°C in Kelvin (273.15 K)
26
- "PI": sci.pi,
27
- "Md": 28.966e-3, # Dry air molar mass (kg/mol)
28
- "Mv": 18.015e-3, # Water vapour molar mass (kg/mol)
29
- "eps": 18.015 / 28.966, # Mv/Md ratio
30
- "g_std": sci.g, # Standard gravity (m/s²)
31
- "c_pd": 1005.0, # Specific heat of dry air at constant pressure (J/kg/K)
32
- "c_pv": 1850.0, # Specific heat of water vapour at constant pressure (J/kg/K)
33
- "rho_w": 1000.0, # Density of liquid water (kg/m³)
34
- "l_tri": 2.5e6, # Latent heat of vaporization at triple point (J/kg)
35
- "MAC": 1.0, # Mass accommodation coefficient
36
- "HAC": 1.0, # Thermal accommodation coefficient
37
- }
38
-
39
- # Flatau-Walko-Cotton saturation vapour pressure coefficients
40
- FWC_COEFFS = {
41
- "C0": 6.115836990e2, # Pa
42
- "C1": 0.444606896e2, # Pa/K
43
- "C2": 0.143177157e1, # Pa/K²
44
- "C3": 0.264224321e-1, # Pa/K³
45
- "C4": 0.299291081e-3, # Pa/K⁴
46
- "C5": 0.203154182e-5, # Pa/K⁵
47
- "C6": 0.702620698e-8, # Pa/K⁶
48
- "C7": 0.379534310e-11, # Pa/K⁷
49
- "C8": -0.321582393e-13, # Pa/K⁸
50
- }
51
-
52
- # Isotope constants (VSMOW standard)
53
- ISOTOPE_CONSTANTS = {
54
- "VSMOW_R_2H": 155.76e-6, # Heavy-to-light isotope abundance ratio for deuterium
55
- "VSMOW_R_3H": 1.85e-17, # For tritium
56
- "VSMOW_R_18O": 2005.20e-6, # For oxygen-18
57
- "VSMOW_R_17O": 379.9e-6, # For oxygen-17
58
- "M_1H": 1.00782503224e-3, # Hydrogen atomic weight (kg/mol)
59
- "M_2H": 2.01410177812e-3, # Deuterium atomic weight (kg/mol)
60
- "M_16O": 15.99491461957e-3, # Oxygen-16 atomic weight (kg/mol)
61
- "M_18O": 17.99915961287e-3, # Oxygen-18 atomic weight (kg/mol)
62
- }
63
-
64
-
65
  @mcp.tool(name="get_physical_constants", description="Retrieve physical constants")
66
  def get_physical_constants() -> dict:
67
  """
@@ -73,26 +34,26 @@ def get_physical_constants() -> dict:
73
  try:
74
  result = {
75
  "fundamental_constants": {
76
- "R_str": {"value": PHYSICAL_CONSTANTS["R_str"], "unit": "J/(K·mol)", "description": "Universal gas constant"},
77
- "N_A": {"value": PHYSICAL_CONSTANTS["N_A"], "unit": "1/mol", "description": "Avogadro constant"},
78
- "g_std": {"value": PHYSICAL_CONSTANTS["g_std"], "unit": "m/s²", "description": "Standard gravity"},
79
- "PI": {"value": PHYSICAL_CONSTANTS["PI"], "unit": "dimensionless", "description": "Pi"},
80
  },
81
  "thermodynamic_constants": {
82
- "T0": {"value": PHYSICAL_CONSTANTS["T0"], "unit": "K", "description": "Zero Celsius in Kelvin"},
83
- "c_pd": {"value": PHYSICAL_CONSTANTS["c_pd"], "unit": "J/(kg·K)", "description": "Specific heat of dry air"},
84
- "c_pv": {"value": PHYSICAL_CONSTANTS["c_pv"], "unit": "J/(kg·K)", "description": "Specific heat of water vapour"},
85
- "l_tri": {"value": PHYSICAL_CONSTANTS["l_tri"], "unit": "J/kg", "description": "Latent heat of vaporization"},
86
  },
87
- "molecular_constants": {
88
- "Md": {"value": PHYSICAL_CONSTANTS["Md"], "unit": "kg/mol", "description": "Dry air molar mass"},
89
- "Mv": {"value": PHYSICAL_CONSTANTS["Mv"], "unit": "kg/mol", "description": "Water vapour molar mass"},
90
- "eps": {"value": PHYSICAL_CONSTANTS["eps"], "unit": "dimensionless", "description": "Mv/Md ratio"},
91
- "rho_w": {"value": PHYSICAL_CONSTANTS["rho_w"], "unit": "kg/m³", "description": "Density of liquid water"},
92
  },
93
- "accommodation_coefficients": {
94
- "MAC": {"value": PHYSICAL_CONSTANTS["MAC"], "unit": "dimensionless", "description": "Mass accommodation coefficient"},
95
- "HAC": {"value": PHYSICAL_CONSTANTS["HAC"], "unit": "dimensionless", "description": "Thermal accommodation coefficient"},
 
 
96
  }
97
  }
98
  return {"success": True, "result": result, "error": None}
@@ -100,52 +61,31 @@ def get_physical_constants() -> dict:
100
  return {"success": False, "result": None, "error": str(e)}
101
 
102
 
103
- # ===================== Saturation Vapour Pressure =====================
104
-
105
- def _pvs_flatau_walko_cotton(T_celsius: float) -> float:
106
- """Calculate saturation vapour pressure using Flatau-Walko-Cotton polynomial."""
107
- C = FWC_COEFFS
108
- return (C["C0"] + T_celsius * (C["C1"] + T_celsius * (C["C2"] + T_celsius * (
109
- C["C3"] + T_celsius * (C["C4"] + T_celsius * (C["C5"] + T_celsius * (
110
- C["C6"] + T_celsius * (C["C7"] + T_celsius * C["C8"]))))))))
111
-
112
-
113
- def _pvs_august_roche_magnus(T_celsius: float) -> float:
114
- """Calculate saturation vapour pressure using August-Roche-Magnus formula."""
115
- # Coefficients from Alduchov & Eskridge 1996
116
- C1 = 610.94 # Pa
117
- C2 = 17.625
118
- C3 = 243.04 # °C
119
- return C1 * math.exp(C2 * T_celsius / (T_celsius + C3))
120
-
121
 
122
  @mcp.tool(name="calculate_saturation_vapour_pressure", description="Calculate saturation vapour pressure over water")
123
- def calculate_saturation_vapour_pressure(temperature_kelvin: float, method: str = "flatau_walko_cotton") -> dict:
124
  """
125
- Calculate saturation vapour pressure over liquid water.
126
 
127
  Parameters:
128
  - temperature_kelvin (float): Temperature in Kelvin.
129
- - method (str): Method to use - 'flatau_walko_cotton' or 'august_roche_magnus'.
130
 
131
  Returns:
132
  - dict: Saturation vapour pressure in Pascals and hectopascals.
133
  """
134
  try:
135
- T_celsius = temperature_kelvin - PHYSICAL_CONSTANTS["T0"]
136
-
137
- if method == "flatau_walko_cotton":
138
- pvs = _pvs_flatau_walko_cotton(T_celsius)
139
- elif method == "august_roche_magnus":
140
- pvs = _pvs_august_roche_magnus(T_celsius)
141
- else:
142
- return {"success": False, "result": None, "error": f"Unknown method: {method}. Use 'flatau_walko_cotton' or 'august_roche_magnus'."}
143
 
144
  result = {
145
  "temperature_K": temperature_kelvin,
146
- "temperature_C": T_celsius,
147
- "saturation_vapour_pressure_Pa": pvs,
148
- "saturation_vapour_pressure_hPa": pvs / 100,
149
  "method": method
150
  }
151
  return {"success": True, "result": result, "error": None}
@@ -153,12 +93,10 @@ def calculate_saturation_vapour_pressure(temperature_kelvin: float, method: str
153
  return {"success": False, "result": None, "error": str(e)}
154
 
155
 
156
- # ===================== Condensation Calculations =====================
157
-
158
  @mcp.tool(name="calculate_condensation", description="Calculate condensation rates")
159
  def calculate_condensation(temperature: float, pressure: float, relative_humidity: float = 1.0) -> dict:
160
  """
161
- Calculate condensation-related parameters.
162
 
163
  Parameters:
164
  - temperature (float): Temperature in Kelvin.
@@ -169,26 +107,28 @@ def calculate_condensation(temperature: float, pressure: float, relative_humidit
169
  - dict: Condensation parameters including supersaturation and vapour pressure.
170
  """
171
  try:
172
- T_celsius = temperature - PHYSICAL_CONSTANTS["T0"]
173
- pvs = _pvs_flatau_walko_cotton(T_celsius)
 
 
174
 
175
  # Actual vapour pressure
176
- pv = relative_humidity * pvs
177
 
178
  # Supersaturation
179
  supersaturation = relative_humidity - 1.0
180
 
181
- # Water vapour mixing ratio
182
- eps = PHYSICAL_CONSTANTS["eps"]
183
- mixing_ratio = eps * pv / (pressure - pv)
184
 
185
  # Specific humidity
186
- specific_humidity = mixing_ratio / (1 + mixing_ratio)
187
 
188
  result = {
189
  "temperature_K": temperature,
190
  "pressure_Pa": pressure,
191
- "saturation_vapour_pressure_Pa": pvs,
192
  "actual_vapour_pressure_Pa": pv,
193
  "relative_humidity": relative_humidity,
194
  "supersaturation": supersaturation,
@@ -216,12 +156,13 @@ def simulate_particles(particle_count: int, time_step: float) -> dict:
216
  - dict: Simulation configuration and recommendations.
217
  """
218
  try:
219
- # PySDM condensation solver defaults
 
220
  defaults = {
221
- "rtol_x": 1e-6, # Relative tolerance for particle size
222
- "rtol_thd": 1e-6, # Relative tolerance for thermodynamic variables
223
- "dt_cond_range": (1e-4, 1.0), # Condensation timestep range (s)
224
- "max_iters": 16, # Maximum iterations for solver
225
  }
226
 
227
  result = {
@@ -255,7 +196,7 @@ def simulate_particles(particle_count: int, time_step: float) -> dict:
255
  @mcp.tool(name="calculate_droplet_volume", description="Calculate droplet volume from radius")
256
  def calculate_droplet_volume(radius_um: float) -> dict:
257
  """
258
- Calculate droplet volume and related properties.
259
 
260
  Parameters:
261
  - radius_um (float): Droplet radius in micrometers.
@@ -264,21 +205,23 @@ def calculate_droplet_volume(radius_um: float) -> dict:
264
  - dict: Volume, surface area, and mass of the droplet.
265
  """
266
  try:
 
267
  radius_m = radius_um * 1e-6
268
- PI = PHYSICAL_CONSTANTS["PI"]
269
- rho_w = PHYSICAL_CONSTANTS["rho_w"]
270
 
271
- volume = (4/3) * PI * radius_m**3
272
- surface_area = 4 * PI * radius_m**2
273
- mass = rho_w * volume
 
 
 
274
 
275
  result = {
276
  "radius_um": radius_um,
277
  "radius_m": radius_m,
278
- "volume_m3": volume,
279
- "volume_um3": volume * 1e18,
280
- "surface_area_m2": surface_area,
281
- "surface_area_um2": surface_area * 1e12,
282
  "mass_kg": mass,
283
  "mass_ng": mass * 1e12
284
  }
@@ -290,7 +233,7 @@ def calculate_droplet_volume(radius_um: float) -> dict:
290
  @mcp.tool(name="calculate_radius_from_volume", description="Calculate droplet radius from volume")
291
  def calculate_radius_from_volume(volume_um3: float) -> dict:
292
  """
293
- Calculate droplet radius from volume.
294
 
295
  Parameters:
296
  - volume_um3 (float): Droplet volume in cubic micrometers.
@@ -299,16 +242,16 @@ def calculate_radius_from_volume(volume_um3: float) -> dict:
299
  - dict: Radius in various units.
300
  """
301
  try:
 
302
  volume_m3 = volume_um3 * 1e-18
303
- PI = PHYSICAL_CONSTANTS["PI"]
304
 
305
- radius_m = (volume_m3 * 3 / (4 * PI)) ** (1/3)
306
- radius_um = radius_m * 1e6
307
 
308
  result = {
309
  "volume_um3": volume_um3,
310
  "volume_m3": volume_m3,
311
- "radius_m": radius_m,
312
  "radius_um": radius_um,
313
  "diameter_um": 2 * radius_um
314
  }
@@ -333,11 +276,13 @@ def calculate_kappa_koehler(dry_radius_um: float, kappa: float, temperature_kelv
333
  - dict: Critical supersaturation and activation radius.
334
  """
335
  try:
 
 
336
  # Physical constants
337
  sigma = 0.072 # Surface tension of water (N/m)
338
- Mv = PHYSICAL_CONSTANTS["Mv"]
339
- rho_w = PHYSICAL_CONSTANTS["rho_w"]
340
- R = PHYSICAL_CONSTANTS["R_str"]
341
  T = temperature_kelvin
342
 
343
  dry_radius_m = dry_radius_um * 1e-6
@@ -346,7 +291,6 @@ def calculate_kappa_koehler(dry_radius_um: float, kappa: float, temperature_kelv
346
  A = 2 * sigma * Mv / (rho_w * R * T)
347
 
348
  # Critical supersaturation (approximation from leading terms)
349
- # S_c ≈ (4 A³ / 27 κ D_dry³)^0.5
350
  S_c = math.sqrt(4 * A**3 / (27 * kappa * dry_radius_m**3))
351
 
352
  # Critical radius
@@ -372,24 +316,26 @@ def calculate_kappa_koehler(dry_radius_um: float, kappa: float, temperature_kelv
372
  @mcp.tool(name="get_isotope_constants", description="Get water isotope constants")
373
  def get_isotope_constants() -> dict:
374
  """
375
- Get water isotope constants (VSMOW standard).
376
 
377
  Returns:
378
  - dict: Isotope abundance ratios and atomic masses.
379
  """
380
  try:
 
 
381
  result = {
382
  "VSMOW_ratios": {
383
- "R_2H": {"value": ISOTOPE_CONSTANTS["VSMOW_R_2H"], "description": "Deuterium abundance ratio"},
384
- "R_3H": {"value": ISOTOPE_CONSTANTS["VSMOW_R_3H"], "description": "Tritium abundance ratio"},
385
- "R_18O": {"value": ISOTOPE_CONSTANTS["VSMOW_R_18O"], "description": "Oxygen-18 abundance ratio"},
386
- "R_17O": {"value": ISOTOPE_CONSTANTS["VSMOW_R_17O"], "description": "Oxygen-17 abundance ratio"},
387
  },
388
  "atomic_masses_kg_per_mol": {
389
- "M_1H": ISOTOPE_CONSTANTS["M_1H"],
390
- "M_2H": ISOTOPE_CONSTANTS["M_2H"],
391
- "M_16O": ISOTOPE_CONSTANTS["M_16O"],
392
- "M_18O": ISOTOPE_CONSTANTS["M_18O"],
393
  },
394
  "description": "VSMOW (Vienna Standard Mean Ocean Water) is the international standard for water isotope ratios"
395
  }
 
10
 
11
  from fastmcp import FastMCP
12
  import numpy as np
13
+
14
+ # Import PySDM modules
15
+ from PySDM.physics import constants as const
16
+ from PySDM.physics import si
17
+ from PySDM.physics.trivia import Trivia
18
+ from PySDM.formulae import Formulae
19
 
20
  # Create the FastMCP service application
21
  mcp = FastMCP("pysdm_service")
 
23
 
24
  # ===================== Physical Constants =====================
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  @mcp.tool(name="get_physical_constants", description="Retrieve physical constants")
27
  def get_physical_constants() -> dict:
28
  """
 
34
  try:
35
  result = {
36
  "fundamental_constants": {
37
+ "R_str": {"value": float(const.sci.R), "unit": "J/(K·mol)", "description": "Universal gas constant"},
38
+ "N_A": {"value": float(const.sci.N_A), "unit": "1/mol", "description": "Avogadro constant"},
39
+ "g_std": {"value": float(const.sci.g), "unit": "m/s²", "description": "Standard gravity"},
40
+ "PI": {"value": float(const.PI), "unit": "dimensionless", "description": "Pi"},
41
  },
42
  "thermodynamic_constants": {
43
+ "T0": {"value": float(const.T0 / si.kelvin), "unit": "K", "description": "Zero Celsius in Kelvin"},
44
+ "sqrt_two": {"value": float(const.sqrt_two), "unit": "dimensionless", "description": "Square root of 2"},
45
+ "sqrt_pi": {"value": float(const.sqrt_pi), "unit": "dimensionless", "description": "Square root of pi"},
 
46
  },
47
+ "numerical_constants": {
48
+ "ONE_THIRD": {"value": float(const.ONE_THIRD), "unit": "dimensionless", "description": "1/3"},
49
+ "TWO_THIRDS": {"value": float(const.TWO_THIRDS), "unit": "dimensionless", "description": "2/3"},
50
+ "PI_4_3": {"value": float(const.PI_4_3), "unit": "dimensionless", "description": "/3"},
 
51
  },
52
+ "concentration_units": {
53
+ "PPM": {"value": float(const.PPM), "unit": "dimensionless", "description": "Parts per million"},
54
+ "PPB": {"value": float(const.PPB), "unit": "dimensionless", "description": "Parts per billion"},
55
+ "PER_CENT": {"value": float(const.PER_CENT), "unit": "dimensionless", "description": "Percent"},
56
+ "PER_MILLE": {"value": float(const.PER_MILLE), "unit": "dimensionless", "description": "Per mille"},
57
  }
58
  }
59
  return {"success": True, "result": result, "error": None}
 
61
  return {"success": False, "result": None, "error": str(e)}
62
 
63
 
64
+ # ===================== Formulae Tools =====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  @mcp.tool(name="calculate_saturation_vapour_pressure", description="Calculate saturation vapour pressure over water")
67
+ def calculate_saturation_vapour_pressure(temperature_kelvin: float, method: str = "FlatauWalkoCotton") -> dict:
68
  """
69
+ Calculate saturation vapour pressure over liquid water using PySDM Formulae.
70
 
71
  Parameters:
72
  - temperature_kelvin (float): Temperature in Kelvin.
73
+ - method (str): Method to use - 'FlatauWalkoCotton', 'AugustRocheMagnus', 'MurphyKoop2005', etc.
74
 
75
  Returns:
76
  - dict: Saturation vapour pressure in Pascals and hectopascals.
77
  """
78
  try:
79
+ formulae = Formulae(saturation_vapour_pressure=method)
80
+ T = temperature_kelvin * si.kelvin
81
+ pvs = formulae.saturation_vapour_pressure.pvs_water(formulae.constants, T)
82
+ pvs_value = float(pvs / si.pascal)
 
 
 
 
83
 
84
  result = {
85
  "temperature_K": temperature_kelvin,
86
+ "temperature_C": temperature_kelvin - 273.15,
87
+ "saturation_vapour_pressure_Pa": pvs_value,
88
+ "saturation_vapour_pressure_hPa": pvs_value / 100,
89
  "method": method
90
  }
91
  return {"success": True, "result": result, "error": None}
 
93
  return {"success": False, "result": None, "error": str(e)}
94
 
95
 
 
 
96
  @mcp.tool(name="calculate_condensation", description="Calculate condensation rates")
97
  def calculate_condensation(temperature: float, pressure: float, relative_humidity: float = 1.0) -> dict:
98
  """
99
+ Calculate condensation-related parameters using PySDM.
100
 
101
  Parameters:
102
  - temperature (float): Temperature in Kelvin.
 
107
  - dict: Condensation parameters including supersaturation and vapour pressure.
108
  """
109
  try:
110
+ formulae = Formulae()
111
+ T = temperature * si.kelvin
112
+ pvs = formulae.saturation_vapour_pressure.pvs_water(formulae.constants, T)
113
+ pvs_value = float(pvs / si.pascal)
114
 
115
  # Actual vapour pressure
116
+ pv = relative_humidity * pvs_value
117
 
118
  # Supersaturation
119
  supersaturation = relative_humidity - 1.0
120
 
121
+ # Water vapour mixing ratio (eps = Mv/Md ≈ 0.622)
122
+ eps = 0.622
123
+ mixing_ratio = eps * pv / (pressure - pv) if pressure > pv else float('nan')
124
 
125
  # Specific humidity
126
+ specific_humidity = mixing_ratio / (1 + mixing_ratio) if not np.isnan(mixing_ratio) else float('nan')
127
 
128
  result = {
129
  "temperature_K": temperature,
130
  "pressure_Pa": pressure,
131
+ "saturation_vapour_pressure_Pa": pvs_value,
132
  "actual_vapour_pressure_Pa": pv,
133
  "relative_humidity": relative_humidity,
134
  "supersaturation": supersaturation,
 
156
  - dict: Simulation configuration and recommendations.
157
  """
158
  try:
159
+ from PySDM.dynamics.condensation import DEFAULTS as COND_DEFAULTS
160
+
161
  defaults = {
162
+ "rtol_x": COND_DEFAULTS.rtol_x,
163
+ "rtol_thd": COND_DEFAULTS.rtol_thd,
164
+ "dt_cond_range": (float(COND_DEFAULTS.cond_range[0] / si.second), float(COND_DEFAULTS.cond_range[1] / si.second)),
165
+ "schedule": COND_DEFAULTS.schedule,
166
  }
167
 
168
  result = {
 
196
  @mcp.tool(name="calculate_droplet_volume", description="Calculate droplet volume from radius")
197
  def calculate_droplet_volume(radius_um: float) -> dict:
198
  """
199
+ Calculate droplet volume and related properties using PySDM Trivia functions.
200
 
201
  Parameters:
202
  - radius_um (float): Droplet radius in micrometers.
 
205
  - dict: Volume, surface area, and mass of the droplet.
206
  """
207
  try:
208
+ formulae = Formulae()
209
  radius_m = radius_um * 1e-6
 
 
210
 
211
+ volume = Trivia.volume(formulae.constants, radius_m)
212
+ surface_area = Trivia.area(formulae.constants, radius_m)
213
+
214
+ # Assuming water density ~1000 kg/m³
215
+ rho_w = 1000.0
216
+ mass = rho_w * float(volume)
217
 
218
  result = {
219
  "radius_um": radius_um,
220
  "radius_m": radius_m,
221
+ "volume_m3": float(volume),
222
+ "volume_um3": float(volume) * 1e18,
223
+ "surface_area_m2": float(surface_area),
224
+ "surface_area_um2": float(surface_area) * 1e12,
225
  "mass_kg": mass,
226
  "mass_ng": mass * 1e12
227
  }
 
233
  @mcp.tool(name="calculate_radius_from_volume", description="Calculate droplet radius from volume")
234
  def calculate_radius_from_volume(volume_um3: float) -> dict:
235
  """
236
+ Calculate droplet radius from volume using PySDM Trivia functions.
237
 
238
  Parameters:
239
  - volume_um3 (float): Droplet volume in cubic micrometers.
 
242
  - dict: Radius in various units.
243
  """
244
  try:
245
+ formulae = Formulae()
246
  volume_m3 = volume_um3 * 1e-18
 
247
 
248
+ radius_m = Trivia.radius(formulae.constants, volume_m3)
249
+ radius_um = float(radius_m) * 1e6
250
 
251
  result = {
252
  "volume_um3": volume_um3,
253
  "volume_m3": volume_m3,
254
+ "radius_m": float(radius_m),
255
  "radius_um": radius_um,
256
  "diameter_um": 2 * radius_um
257
  }
 
276
  - dict: Critical supersaturation and activation radius.
277
  """
278
  try:
279
+ formulae = Formulae(hygroscopicity="KappaKoehlerLeadingTerms")
280
+
281
  # Physical constants
282
  sigma = 0.072 # Surface tension of water (N/m)
283
+ Mv = 18.015e-3 # kg/mol
284
+ rho_w = 1000.0 # kg/m³
285
+ R = const.sci.R
286
  T = temperature_kelvin
287
 
288
  dry_radius_m = dry_radius_um * 1e-6
 
291
  A = 2 * sigma * Mv / (rho_w * R * T)
292
 
293
  # Critical supersaturation (approximation from leading terms)
 
294
  S_c = math.sqrt(4 * A**3 / (27 * kappa * dry_radius_m**3))
295
 
296
  # Critical radius
 
316
  @mcp.tool(name="get_isotope_constants", description="Get water isotope constants")
317
  def get_isotope_constants() -> dict:
318
  """
319
+ Get water isotope constants (VSMOW standard) from PySDM.
320
 
321
  Returns:
322
  - dict: Isotope abundance ratios and atomic masses.
323
  """
324
  try:
325
+ from PySDM.physics import constants_defaults as cd
326
+
327
  result = {
328
  "VSMOW_ratios": {
329
+ "R_2H": {"value": float(cd.VSMOW_R_2H), "description": "Deuterium abundance ratio"},
330
+ "R_3H": {"value": float(cd.VSMOW_R_3H), "description": "Tritium abundance ratio"},
331
+ "R_18O": {"value": float(cd.VSMOW_R_18O), "description": "Oxygen-18 abundance ratio"},
332
+ "R_17O": {"value": float(cd.VSMOW_R_17O), "description": "Oxygen-17 abundance ratio"},
333
  },
334
  "atomic_masses_kg_per_mol": {
335
+ "M_1H": float(cd.M_1H / (si.g / si.mole)),
336
+ "M_2H": float(cd.M_2H / (si.g / si.mole)),
337
+ "M_16O": float(cd.M_16O / (si.g / si.mole)),
338
+ "M_18O": float(cd.M_18O / (si.g / si.mole)),
339
  },
340
  "description": "VSMOW (Vienna Standard Mean Ocean Water) is the international standard for water isotope ratios"
341
  }