Joule heat calculator
mortylen
Posted on October 22, 2023
I would like to learn programming in the Rust language, and there is no better way to start than by developing an application directly in Rust. I need to write an application for scientific purposes that will calculate the heating of a wire. I chose Rust for this application because it is a multiplatform language known for its security features, and I am eager to enhance my skills in it.
Perhaps this article will be more technical. I will describe some aspects of physics and mathematics, but rest assured, I will also give due attention to the design of the application and the process of writing the code.
About project
For starters, let me explain the purpose of this project. I aim to develop software that calculates wire heating. This application will serve scientific purposes, specifically for the research and development of superconductors. The wire under consideration will operate at cryogenic temperatures and the wire will experience a single current pulse. As the electric current flows through the wire, it will generate heat, but the surrounding environment will also contribute to temperature dissipation. Therefore, it is essential to calculate the wire's temperature over time, taking into account both the heating from the current and the cooling effects from the environment.
Physics, only briefly
Joule heat refers to the heat generated within an electric conductor when an electric current flows through it. It represents the direct conversion of electrical energy into internal energy. The heating of the conductor can be explained by the transfer of kinetic energy from the particles involved in the electric current (electrons) to particles that do not directly participate in the current (positive ions in fixed positions). This transfer of energy leads to an increase in the thermal motion of these particles, causing the conductor to heat up.
It is important to remember that the conductor not only heats up but also cools down. Heat energy is transferred to the surrounding environment. Heat exchange occurs in a manner where the warmer medium transfers a part of its internal energy to the cooler medium or the environment. Therefore, the heat transfer must be taken into account and subtracted from the heating process during calculations.
Mathematics, I have to explain this a little bit
I have this mathematical formula:
If I want to track the heating process over time, it is necessary to break down the calculation into numerous small steps or iterations. The more iterations there are, the more accurate and smoother the calculation becomes. In each iteration, the formula will be computed, and the result (delta T) will be added to the current temperature of the sample.
In the calculation, the following variables are involved:
I - DC electric current [A] depends on the time
R - sample resistance [ohm] depends on temperature
A - the surface area of the sample [m^2]
h - heat transfer coefficient dependent on temperature
Tsurf - initial surface temperature of the sample [K]
Tp - temperature of the environment [K]
m - weight of the sample [kg]
c - mass thermal capacity depends on temperature
t - iteration time interval [s]
e - Euler's number (2.718281828459...)
The value of I (DC electric current) will be selected from a table that represents the current values at different points in time. This table will have two columns, one for time and the other for the corresponding current value. Essentially, it represents the waveform of the current pulse.
Similarly, the values for R (resistance), h (heat transfer coefficient), and c (mass thermal capacity) will be selected using a similar approach, but the waveform will depend on temperature instead of time.
The other variables, such as A (surface area) and m (weight), remain constant for a specific sample. Additionally, Tsurf (initial surface temperature) and Tp (environmental temperature) are the initial constants required for the calculation. The time interval, t, is calculated by dividing the pulse duration by the number of iterations. And e (Euler's number) is used for the exponential calculation.
Design of the application
The joule heating calculation application is a console application developed in Rust. Its design follows a straightforward approach, making it easy to understand and use. The application starts by reading a prepared configuration file, which contains the necessary input parameters for the calculation. This allows users to customize the settings without modifying the application's code.
Once the configuration file is loaded, the application initiates the calculation process.
After the calculation is completed, the application stores the calculated results in a file for the possibility to work with the results in different analytical tools.
The application assumes that the user has pre-calculated the characteristics of a current table, a resistance table, a specific heat table, and a heat transfer table.
Create configuration files
Firstly, it is necessary to consider the configuration file, from which the application will read all the essential parameters required for the calculation. The user will have the flexibility to edit this configuration file at any time according to their specific needs. I have chosen the TOML format for this purpose. Within the file, initial values for the calculation will be provided, along with references to tables containing relevant data such as current, resistance, heat transfer coefficients, and mass thermal capacity. These tables will be stored in either TOML or CSV format for ease of access and readability.
The main setting file looks like this:
app_setting.toml
# Set resistance table [toml] file path 'resistance_tbl = [{index = 0.0, value=0.0}]' or as CSV file
resistance_tbl_path = "/home/runner/joule-heat-rust/src/setting/resistance_tbl.csv"
# Set specific heat table [toml] file path 'specific_heat_tbl = [{index = 0.0, value=0.0}]' or as CSV file
specific_heat_tbl_path = "/home/runner/joule-heat-rust/src/setting/specific_heat_tbl.csv"
# Set heat transfer table [toml] file path 'heat_transfer_tbl = [{index = 0.0, value=0.0}]' or as CSV file
heat_transfer_tbl_path = "/home/runner/joule-heat-rust/src/setting/heat_transfer_tbl.csv"
# Set current table [toml] file path 'current_tbl = [{index = 0.0, value=0.0}]' or as CSV file
current_tbl_path = "/home/runner/joule-heat-rust/src/setting/current_tbl.csv"
# Set sample surface area [mm^2]
surface_area = 70.591586
# Set sample weight [g]
weight = 0.17037
# Set start sample temperature [K]
start_sample_temperature = 77.0
# Set enviroment temperature [K]
enviroment_temperature = 77.0
# Set pulse duration [ms]
pulse_duration = 1000
# Set number of iterations
num_of_iterations = 10000
# Set export directory path
export_path = "/home/runner/joule-heat-rust/src/test.csv"
Tables with data like current, resistance, heat transfer and mass thermal capacity look like this:
current_tbl.toml
index_value_data = [
{index=1,value=10},
{index=10,value=20},
{index=20,value=50},
{index=30,value=100},
{index=50,value=200},
{index=1000,value=200}
]
or as CSV like this:
current_tbl.csv
index, value
1, 10
10, 20
20, 50
30, 100
50, 200
1000, 200
current_tbl: Contains the DC electric current data depending on time. The 'index' represents the time data [ms] and the 'value' represents the current value [A].
resistance_tbl: Contains the resistance of the sample depending on temperature. The 'index' represents the temperature data [K] and the 'value' represents the resistance value [ohm].
specific_heat_tbl: Contains the mass thermal capacity depending on temperature. Heat capacity is a property that describes how much heat energy is required to raise the temperature if a given sample. The 'index' represents the temperature data [K] and the 'value' represents the heat capacity.
heat_transfer_tbl: Contains the heat transfer coefficient depending on temperature. Is the proportionality constant between the heat flux and the thermodynamic driving force for the flow of heat. The 'index' represents the temperature data [K] and the 'value' represents the heat transfer coefficient.
The path to this configuration file is either sent as an argument when the application is launched, or it can be directly set within the application.
Source Code Analysis
This first version of the application is very simple. The basic principle is described in the paragraph Design of the application. Now I will describe in more detail the single parts of the source code.
First I need to prepare the Cargo.toml file. I need the following dependencies:
serde - for serializing and deserializing data structures
toml - for serializing and deserializing toml file format
chrono - for working with time
csv - for serializing and deserializing CSV file format
Cargo.toml
[package]
name = "joule-heat-rust"
version = "0.1.0"
authors = ["mortylen"]
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"]}
toml = "0.5"
chrono = "0.4.24"
csv = "1.1.6"
In the main.rs file, I've included the necessary imports that I'll require in various sections of the code. These imports are added to ensure that the required functionalities and libraries are available throughout the codebase. While some may not be directly utilized in the initial sections of the code.
use std::io;
use std::path::Path;
use std::fs;
use std::error::Error;
use std::env;
use serde::Deserialize;
use toml;
use csv::Writer;
use chrono::Utc;
Next, I've defined a constant that holds the path to the default configuration file.
const CONFIG_FILE_NAME_PATH: &str = "app_setting.toml";
Following this, I created a structure to encapsulate all the data from the configuration file. Additionally, I implemented a function named build that deserializes the data from the file into the Config structure. The build function takes the content of the configuration file as a parameter, deserializes it using the toml library, and returns an instance of the Config structure.
struct Config {
resistance_tbl_path: String,
specific_heat_tbl_path: String,
heat_transfer_tbl_path: String,
current_tbl_path: String,
export_path: String,
surface_area: f64,
weight: f64,
start_sample_temperature: f64,
enviroment_temperature: f64,
pulse_duration: f64,
num_of_iterations: u64,
}
impl Config {
fn build(file_content: &String) -> Config {
let cfg: Config = match toml::from_str(&file_content) {
Ok(cfg) => cfg,
Err(error) => panic!("Problem parsing config file: '{}'. {}", &CONFIG_FILE_NAME_PATH, error),
};
cfg
}
}
Next, I proceeded to create structures to represent individual rows and tables that would store values from various tables, including current, resistance, heat transfer coefficient or mass thermal capacity.
The RowIndexValue structure is designed to hold an index and its corresponding value. This structure is used to represent a single entry within a table.
The TblIndexValueData structure is used to store a collection of RowIndexValue instances, effectively representing a table of index-value pairs.
struct RowIndexValue {
index: f64,
value: f64,
}
struct TblIndexValueData {
index_value_data: Vec<RowIndexValue>,
}
I implemented several essential functions within the TblIndexValueData structure.
The fill_tbl_index_value function takes the content of the configuration file as an argument and returns a filled instance of the TblIndexValueData structure. It detects whether the content is in CSV or TOML format based on its initial characters.
The get_down_index_value function accepts a numeric index as input. It searches the table for a value that either matches the provided index or is lower. The function returns an Option containing a tuple of the index and value if found, or None if no suitable value is present.
The get_up_index_value function is similar to get_down_index_value, except it searches for values equal to or higher than the provided index.
The get_delta function calculates the difference between two numeric values, given a lower and upper value.
The calculate_value_by_index function takes an index as input and calculates the exact value corresponding to the specified index using the aforementioned functions.
impl TblIndexValueData {
fn fill_tbl_index_value(file_content: &String) -> TblIndexValueData {
let mut tbl_data = TblIndexValueData {
index_value_data: Vec::new(),
};
if file_content.starts_with("index_value_data") { //toml file
tbl_data = match toml::from_str(&file_content) {
Ok(tbl_data) => tbl_data,
Err(error) => panic!("Problem parsing config file: {}", error),
};
}
else if file_content.starts_with("index") { //csv file
let mut reader = csv::Reader::from_reader(file_content.as_bytes());
for result in reader.deserialize::<RowIndexValue>() {
match result {
Ok(record) => tbl_data.index_value_data.push(record),
Err(err) => {
eprintln!("Error while deserializing row: {}", err);
}
}
}
}
tbl_data
}
fn get_down_index_value(&self, index: f64) -> Option<(f64, f64)> {
match self.index_value_data.iter().find(|&x| x.index <= index) {
Some(value) => Some((value.index, value.value)),
None => {
if let Some(first_value) = self.index_value_data.first() {
Some((first_value.index, first_value.value))
} else {
None
}
}
}
}
fn get_up_index_value(&self, index: f64) -> Option<(f64, f64)> {
match self.index_value_data.iter().find(|&x| x.index >= index) {
Some(value) => Some((value.index, value.value)),
None => {
if let Some(last_value) = self.index_value_data.last() {
Some((last_value.index, last_value.value))
} else {
None
}
}
}
}
fn get_delta(down_number: f64, up_number: f64) -> f64 {
let delta_number = if up_number == down_number {
down_number
} else {
up_number - down_number
};
delta_number
}
fn calculate_value_by_index(&self, index: f64) -> f64 {
let (down_index, down_value) = self.get_down_index_value(index).unwrap_or_else(|| {
println!("Index search error 'get_down_index_value' for index: {}", index);
io::stdin().read_line(&mut String::new()).unwrap();
panic!("Application terminate.");
});
let (up_index, up_value) = self.get_up_index_value(index).unwrap_or_else(|| {
println!("Index search error 'get_up_index_value' for index: {}", index);
io::stdin().read_line(&mut String::new()).unwrap();
panic!("Application terminate.");
});
let delta_index = TblIndexValueData::get_delta(down_index, up_index);
let delta_value = TblIndexValueData::get_delta(down_value, up_value);
((index - down_index) / delta_index) * delta_value + down_value
}
}
Example: Consider a scenario where the index is equal to 1.5. In this case, the get_down_index_value function will return the value 12, indicating that the value associated with an index less than or equal to 1.5 is 12. Similarly, the get_up_index_value function will return the value 14, indicating that the value associated with an index greater than or equal to 1.5 is 14. Using these values, the calculate_value_by_index function performs its calculation, which results in the value 13.
index,value
0,10
1,12
2,14
3,16
Another important structure within the application is named ExportData. This structure serves as a simple container to store the calculated data. It holds essential information, specifically the time and temperature data.
struct ExportData {
time: f64,
temperature: f64,
heating: f64,
cooling: f64,
}
Before delving into the calculation and the main function, let's explore some auxiliary functions that contribute to the functionality of the application.
The read_config_file function is responsible for reading and returning the contents of the configuration file based on the specified path.
The set_config_path function accepts the path to the configuration file as input. If the file exists, it prompts the user to decide whether to use it or specify a different configuration file path. If the file does not exist, the function guides the user to provide the configuration file path.
The get_user_input_path function prompts the user to enter the path to the configuration file manually.
The export_data_to_csv function plays a role in saving the generated data to a CSV file. It takes the calculated data and configuration as arguments. The function constructs a file name based on the current date and time, appends it to the export path, and writes the data to the CSV file.
fn read_config_file(config_path: &str) -> Result<String, io::Error> {
match fs::read_to_string(&config_path) {
Ok(file_content) => Ok(file_content),
Err(error) => Err(error),
}
}
fn set_config_path(config_file_path: &String) -> String {
if Path::new(&config_file_path).exists() {
let mut user_input = String::new();
println!("Load data settings from: {} [Y/N]", &config_file_path);
io::stdin().read_line(&mut user_input).unwrap();
if user_input.trim().to_lowercase() == "y" {
config_file_path.to_string()
} else {
get_user_input_path()
}
} else {
get_user_input_path()
}
}
fn get_user_input_path() -> String {
let mut user_input = String::new();
println!("Enter setting file path: ");
io::stdin().read_line(&mut user_input).unwrap();
user_input.trim().to_string()
}
fn export_data_to_csv(data: &[ExportData], config: &Config) -> Result<(), io::Error> {
let datetime_now: String = Utc::now().format("_%Y%m%d-%H%M%S").to_string();
let writer_result = Writer::from_path(&config.export_path.replace(".csv", &(datetime_now + ".csv")));
let mut writer = writer_result?;
writer.write_record(&["Time", "Temperature", "Heating", "Cooling"])?;
for item in data {
writer.serialize((
item.time,
item.temperature,
item.heating,
item.cooling,
))?;
}
writer.flush()?;
Ok(())
}
Now it is time to describe the function for calculating the joule heat.
The get_calculated_data function takes a Config object as input and calculates the temperature changes in a wire. Includes data from various input tables like current, resistance, specific heat, and heat transfer coefficients. The calculations are performed iteratively over a defined number of steps, considering parameters like surface area, weight, environment temperature, pulse duration, and more. The function computes the temperature change, accounting for heating and cooling effects, and returns a vector of ExportData representing time-evolving temperature, heating, cooling, and time data.
fn get_calculated_data(config: &Config) -> Result<Vec<ExportData>, Box<dyn Error>> {
let tbl_current = TblIndexValueData::fill_tbl_index_value(&read_config_file(&config.current_tbl_path)?);
let tbl_resistance = TblIndexValueData::fill_tbl_index_value(&read_config_file(&config.resistance_tbl_path)?);
let tbl_specific_heat = TblIndexValueData::fill_tbl_index_value(&read_config_file(&config.specific_heat_tbl_path)?);
let tbl_heat_transfer = TblIndexValueData::fill_tbl_index_value(&read_config_file(&config.heat_transfer_tbl_path)?);
let e = 2.71828182845904523536; //Euler's number
let A = &config.surface_area / 1000000.0; //Surface area [m^2]
let m = &config.weight / 1000.0; //Weight [kg]
let Tp = &config.enviroment_temperature; //Temperature of environment
let dTime = (&config.pulse_duration / 1000.0) / (config.num_of_iterations as f64); //Delta time [s]
let mut temperature = config.start_sample_temperature; //Temperature of sample
let mut dT: f64; //Delta temperature
let mut heating: f64; //= ((current^2 * resistance) / mc) * dTime
let mut cooling: f64; //= ((Ah * (temperature - Tp)) / mc) * dTime
let mut tau_euler_coef: f64; //= 1-e^(-tAh / mc)
let mut mc: f64; //= m * specific_heat
let mut Ah: f64; //= A * heat_transfer
let mut time: f64; //= dTime * i
let mut export_data: Vec<ExportData> = Vec::new();
for i in 0..config.num_of_iterations {
time = dTime * i as f64;
mc = m * tbl_specific_heat.calculate_value_by_index(temperature);
Ah = A * tbl_heat_transfer.calculate_value_by_index(temperature);
heating = ((f64::powf(tbl_current.calculate_value_by_index(temperature), 2.0) * tbl_resistance.calculate_value_by_index(temperature)) / mc) * dTime;
cooling = ((Ah * (temperature - Tp)) / mc) * dTime;
tau_euler_coef = 1.0 - f64::powf(e, -((Ah * time) / mc));
dT = heating - (cooling * tau_euler_coef);
temperature += dT;
export_data.push(ExportData {
time,
temperature,
heating,
cooling
});
}
Ok(export_data)
}
Finally, the last step is to execute the entire process within the main function. The main function is organized into three pivotal stages. First, it reads the configuration file, then it initiates the calculation process, and lastly, it stores the generated data.
fn main() -> Result<(), Box<dyn Error>> {
let args: Vec<String> = env::args().collect();
let config_file_path: String = if args.len() > 1 {
args[1].to_string()
} else {
CONFIG_FILE_NAME_PATH.to_string()
};
//Set config
let config = match read_config_file(&set_config_path(&config_file_path)) {
Ok(file_content) => Config::build(&file_content),
Err(error) => {
println!("Error reading config file: {}. {}", &config_file_path, error);
io::stdin().read_line(&mut String::new()).unwrap();
panic!("Application terminate.");
}
};
//Run calculation
let calculated_data: Vec<ExportData> = match get_calculated_data(&config) {
Ok(data) => {
data
}
Err(error) => {
println!("Calculation error: {}", error);
io::stdin().read_line(&mut String::new()).unwrap();
panic!("Application terminate.");
}
};
//Export data to CSV file
if let Err(error) = export_data_to_csv(&calculated_data, &config) {
println!("Error exporting data to CSV, check 'export_path' value in setting.toml file. {}", error);
} else {
println!("Data exported successfully!");
}
let mut user_input = String::new();
println!("Complete... Press any key to close.");
io::stdin().read_line(&mut user_input).unwrap();
Ok(())
}
Conclusion
This project has offered me a valuable introduction to Rust programming and a wealth of fresh insights. The article delineates the evolution of the initial version of the application. As time advances, I aspire to augment the application's functionality. For the latest updates, I encourage you to peruse my GitHub repository. I hope that this project serves not only as a tool for joule heat calculations but also as an inspiration for the creation of similar applications.
Posted on October 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.