|
| 1 | +# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +"""CoolProp MCP tool for thermophysical fluid properties. |
| 16 | +
|
| 17 | +Wraps the ``CoolProp`` library to look up density, viscosity, conductivity, |
| 18 | +specific heat, and other properties for 124 fluids. No API key required. |
| 19 | +
|
| 20 | +Prerequisites: |
| 21 | + pip install CoolProp |
| 22 | +
|
| 23 | +Usage: |
| 24 | + ++tool_modules=[nemo_skills.mcp.servers.coolprop_tool::CoolPropTool] |
| 25 | +""" |
| 26 | + |
| 27 | +import logging |
| 28 | +from typing import Annotated, Any |
| 29 | + |
| 30 | +from pydantic import Field |
| 31 | + |
| 32 | +from nemo_skills.mcp.tool_manager import Tool |
| 33 | + |
| 34 | +logger = logging.getLogger(__name__) |
| 35 | + |
| 36 | +PROPERTY_DESCRIPTIONS = { |
| 37 | + "D": "Density [kg/m^3]", |
| 38 | + "H": "Specific enthalpy [J/kg]", |
| 39 | + "S": "Specific entropy [J/(kg*K)]", |
| 40 | + "C": "Specific heat at constant pressure Cp [J/(kg*K)]", |
| 41 | + "CVMASS": "Specific heat at constant volume Cv [J/(kg*K)]", |
| 42 | + "V": "Dynamic viscosity [Pa*s]", |
| 43 | + "L": "Thermal conductivity [W/(m*K)]", |
| 44 | + "P": "Pressure [Pa]", |
| 45 | + "T": "Temperature [K]", |
| 46 | + "Q": "Vapor quality [-]", |
| 47 | + "SPEED_OF_SOUND": "Speed of sound [m/s]", |
| 48 | + "SURFACE_TENSION": "Surface tension [N/m]", |
| 49 | + "PRANDTL": "Prandtl number [-]", |
| 50 | + "ISENTROPIC_EXPANSION_COEFFICIENT": "Isentropic expansion coefficient [-]", |
| 51 | +} |
| 52 | + |
| 53 | + |
| 54 | +def fluid_property( |
| 55 | + fluid: Annotated[str, Field(description="Fluid name (e.g. 'Water', 'Nitrogen', 'R134a', 'CO2').")], |
| 56 | + output_property: Annotated[ |
| 57 | + str, |
| 58 | + Field( |
| 59 | + description=( |
| 60 | + "Property to calculate. Common codes: " |
| 61 | + "D (density), C (Cp), CVMASS (Cv), H (enthalpy), S (entropy), " |
| 62 | + "V (viscosity), L (conductivity), SPEED_OF_SOUND, PRANDTL." |
| 63 | + ) |
| 64 | + ), |
| 65 | + ], |
| 66 | + temperature: Annotated[float, Field(description="Temperature in Kelvin.")], |
| 67 | + pressure: Annotated[float, Field(description="Pressure in Pascals.")], |
| 68 | +) -> str: |
| 69 | + """Calculate a thermophysical property of a fluid at given temperature and pressure (SI units).""" |
| 70 | + import CoolProp.CoolProp as CP |
| 71 | + |
| 72 | + if temperature <= 0: |
| 73 | + return "Temperature must be positive (in Kelvin)." |
| 74 | + if pressure <= 0: |
| 75 | + return "Pressure must be positive (in Pascals)." |
| 76 | + |
| 77 | + try: |
| 78 | + value = CP.PropsSI(output_property, "T", temperature, "P", pressure, fluid) |
| 79 | + except ValueError as e: |
| 80 | + return f"CoolProp error for {fluid}: {e}" |
| 81 | + |
| 82 | + desc = PROPERTY_DESCRIPTIONS.get(output_property, output_property) |
| 83 | + return f"**{fluid}** at T={temperature} K, P={pressure} Pa\n{desc}: {value:.6g}" |
| 84 | + |
| 85 | + |
| 86 | +def fluid_list() -> str: |
| 87 | + """List all fluids available in CoolProp.""" |
| 88 | + import CoolProp.CoolProp as CP |
| 89 | + |
| 90 | + fluids = sorted(CP.FluidsList()) |
| 91 | + return f"**{len(fluids)} fluids available:**\n" + ", ".join(fluids) |
| 92 | + |
| 93 | + |
| 94 | +class CoolPropTool(Tool): |
| 95 | + def __init__(self) -> None: |
| 96 | + self._config: dict[str, Any] = {} |
| 97 | + |
| 98 | + def default_config(self) -> dict[str, Any]: |
| 99 | + return dict(self._config) |
| 100 | + |
| 101 | + def configure(self, overrides: dict[str, Any] | None = None, context: dict[str, Any] | None = None) -> None: |
| 102 | + if overrides: |
| 103 | + self._config.update(overrides) |
| 104 | + |
| 105 | + async def list_tools(self) -> list[dict[str, Any]]: |
| 106 | + return [ |
| 107 | + { |
| 108 | + "name": "fluid-property", |
| 109 | + "description": "Calculate a thermophysical property of a fluid at temperature and pressure in SI units.", |
| 110 | + "input_schema": { |
| 111 | + "type": "object", |
| 112 | + "properties": { |
| 113 | + "fluid": {"type": "string", "description": "Fluid name, e.g. Water, Nitrogen, R134a, CO2."}, |
| 114 | + "output_property": { |
| 115 | + "type": "string", |
| 116 | + "description": "CoolProp output property code, e.g. D, C, H, S, V, L.", |
| 117 | + }, |
| 118 | + "temperature": {"type": "number", "description": "Temperature in Kelvin."}, |
| 119 | + "pressure": {"type": "number", "description": "Pressure in Pascals."}, |
| 120 | + }, |
| 121 | + "required": ["fluid", "output_property", "temperature", "pressure"], |
| 122 | + }, |
| 123 | + }, |
| 124 | + { |
| 125 | + "name": "fluid-list", |
| 126 | + "description": "List fluids available in CoolProp.", |
| 127 | + "input_schema": {"type": "object", "properties": {}}, |
| 128 | + }, |
| 129 | + ] |
| 130 | + |
| 131 | + async def execute(self, tool_name: str, arguments: dict[str, Any], extra_args: dict[str, Any] | None = None): |
| 132 | + arguments = dict(arguments or {}) |
| 133 | + if tool_name == "fluid-property": |
| 134 | + return fluid_property(**arguments) |
| 135 | + if tool_name == "fluid-list": |
| 136 | + return fluid_list() |
| 137 | + return f"Error: unknown tool '{tool_name}'" |
0 commit comments