diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index a10f942..1e7fecd 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -5,10 +5,8 @@ on: branches: - main - develop - push: - branches: - - main - - develop + release: + types: [published] jobs: lint: @@ -40,7 +38,6 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ - libwebkit2gtk-4.0-dev \ build-essential \ curl \ wget \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..64f2d42 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: OSCPS_Release_Workflow + +on: + workflow_dispatch: # Enables manual triggering from GitHub Actions UI + +jobs: + build: + name: Build and Release + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build release binary + run: cargo build --release + + - name: Compress binary + run: | + mkdir -p release + cp target/release/your-binary-name release/ + cd release + tar -czvf your-binary-name-linux.tar.gz your-binary-name + shell: bash + + - name: Create GitHub Release and Upload Binary + uses: softprops/action-gh-release@v2 + with: + files: release/your-binary-name-linux.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/rust-tests.yml b/.github/workflows/rust-tests.yml index bd4aeb0..6bc022b 100644 --- a/.github/workflows/rust-tests.yml +++ b/.github/workflows/rust-tests.yml @@ -1,10 +1,6 @@ name: Rust Tests on: - push: - branches: - - main - - develop pull_request: branches: - main @@ -45,7 +41,6 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ - libwebkit2gtk-4.0-dev \ build-essential \ curl \ wget \ diff --git a/Cargo.toml b/Cargo.toml index 4617601..7ca18e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = [ +members = [ "oscps-gui", "oscps-lib", ] diff --git a/README.md b/README.md index 5148539..0427d72 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# OSCPS (Open Sourced Chemical Engineering Process Simulator) +# OSCPS (Open Source Chemical Process Simulator) -![Rust Tests](https://github.com/OSCPS-Project/OSCPS/actions/workflows/rust-tests.yml/badge.svg) -![Documentation](https://github.com/OSCPS-Project/OSCPS/actions/workflows/check-docs.yml/badge.svg) +![Rust Tests](https://github.com/OSCPS-Project/OSCPS/actions/workflows/rust-tests.yml/badge.svg?branch=develop) +![Documentation](https://github.com/OSCPS-Project/OSCPS/actions/workflows/check-docs.yml/badge.svg?branch=develop) -Developing a dynamic & steady-state chemical process simulator using a Rust backend and a iced-rs frontend. This project aims to create a much better version of ASPEN that will also be open-sourced. +Developing a dynamic & steady-state chemical process simulator using a Rust-based backend and frontend. This project aims to create a much better version of ASPEN that will also be open-sourced. ## Authors diff --git a/oscps-gui/Cargo.toml b/oscps-gui/Cargo.toml index 8eb9129..0822647 100644 --- a/oscps-gui/Cargo.toml +++ b/oscps-gui/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "oscps-gui" version = "0.1.0" +authors = ["Nathaniel Thomas ", "Bhargav Akula "] edition = "2021" [dependencies] -iced = "0.13.1" +env_logger = "0.11.6" +iced = {version = "0.13.1", features = ["canvas", "debug", "lazy"]} +log = "0.4" diff --git a/oscps-gui/src/flowsheet.rs b/oscps-gui/src/flowsheet.rs new file mode 100644 index 0000000..6a18a06 --- /dev/null +++ b/oscps-gui/src/flowsheet.rs @@ -0,0 +1,353 @@ +use iced::mouse; +use iced::widget::canvas::event::{self, Event}; +use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; +use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; + +use log::{info, debug}; + +#[derive(Default)] +pub struct State { + cache: canvas::Cache, + pub placement_mode: BlockPlacement, +} + +impl State { + pub fn view<'a>(&'a self, curves: &'a [Curve]) -> Element<'a, Curve> { + Canvas::new(Flowsheet { + state: self, + curves, + }) + .width(Fill) + .height(Fill) + .height(Fill) + .into() + } + pub fn request_redraw(&mut self) { + self.cache.clear(); + } +} + +// Allows detection of "block placement mode", as well as which block to place. +#[derive(Debug, Clone, Copy)] +pub enum BlockPlacement { + Connector, + Mixer, + Default, +} + +impl Default for BlockPlacement { + fn default() -> Self { + BlockPlacement::Default + } +} + +struct Flowsheet<'a> { + state: &'a State, + curves: &'a [Curve], +} + +impl<'a> canvas::Program for Flowsheet<'a> { + type State = Option; + fn update( + &self, + state: &mut Self::State, + event: Event, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> (event::Status, Option) { + let Some(cursor_position) = cursor.position_in(bounds) else { + return (event::Status::Ignored, None); + }; + match event { + Event::Mouse(mouse_event) => { + let message = match mouse_event { + mouse::Event::ButtonPressed(mouse::Button::Left) => { + info!("Click detected at ({})", cursor_position); + match self.state.placement_mode { + BlockPlacement::Connector => { + match *state { + None => { + info!("Beginning creation of connector..."); + let mut result = Some(Pending::One { + from: cursor_position, + }); + for curve in self.curves { + if !matches!(curve, Curve::Connector{..}) && curve.on_output_connector(cursor_position) { + info!("Connected to input!"); + result = Some(Pending::One { + from: curve.get_output_point() + }); + break; + } + } + *state = result; + None + } + Some(Pending::One { from }) => { + info!("Created connector."); + *state = None; + let mut result = Some(Curve::Connector { + from, + to: cursor_position + }); + for curve in self.curves { + if !matches!(curve, Curve::Connector{..}) && curve.on_input_connector(cursor_position) { + info!("Connected to input!"); + result = Some(Curve::Connector { + from, + to: curve.get_input_point() + }); + break; + } + } + result + } + // Some(Pending::Two { from, to }) => { + // *state = None; + // Some(Curve::Connector { + // from, + // to, + // }) + // } + } + }, + BlockPlacement::Mixer => { + let input_point = Point::new(cursor_position.x - 5.0, cursor_position.y + 50.0); + let output_point = Point::new(cursor_position.x + 105.0, cursor_position.y + 50.0); + info!("Creating mixer at ({}, {}) with input at ({}, {}) and output at ({}, {})", cursor_position.x, cursor_position.y, input_point.x, input_point.y, output_point.x, output_point.y); + Some(Curve::Mixer { + at: cursor_position, + input_point, + output_point, + }) + }, + BlockPlacement::Default => { + // TODO: Add code for selecting stuff + None + } + } + + }, + // Right click should cancel placement. + mouse::Event::ButtonPressed(mouse::Button::Right) => { + info!("Right mouse button clicked"); + *state = None; + None + } + _ => None, + }; + (event::Status::Captured, message) + }, + Event::Keyboard(_) => { + (event::Status::Captured, None) + } + _ => (event::Status::Ignored, None), + } + } + + fn draw( + &self, + state: &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Vec { + let content = + self.state.cache.draw(renderer, bounds.size(), |frame| { + Curve::draw_all(self.curves, frame, theme); + frame.stroke( + &Path::rectangle(Point::ORIGIN, frame.size()), + Stroke::default() + .with_width(20.0) + .with_color(theme.palette().text), + ); + }); + if let Some(pending) = state { + vec![content, pending.draw(renderer, theme, bounds, cursor)] + } else { + vec![content] + } + } + fn mouse_interaction( + &self, + _state: &Self::State, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> mouse::Interaction { + if cursor.is_over(bounds) { + mouse::Interaction::Crosshair + } else { + mouse::Interaction::default() + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Curve { + Connector { + from: Point, + to: Point, + }, + Mixer { + at: Point, + input_point: Point, + output_point: Point, + } +} + +impl Curve { + fn on_input_connector( + &self, + cursor_position: Point + ) -> bool { + // TODO: Fix arbitrary 5-pixel bounding box. Ideally use a circular bound. + let input = self.get_input_point(); + info!("Checking input bounds with cursor at ({}, {})", cursor_position.x, cursor_position.y); + if cursor_position.x > input.x - 5.0 && cursor_position.x < input.x + 5.0 { + debug!("Bound x match!"); + if cursor_position.y > input.y - 5.0 && cursor_position.y < input.y + 5.0 { + info!("Bounds match!"); + return true + } + } + false + } + + fn get_input_point(&self) -> Point { + return match self { + Curve::Connector{from, ..} => { + *from + }, + Curve::Mixer{input_point, ..} => { + *input_point + }, + } + } + + fn on_output_connector( + &self, + cursor_position: Point + ) -> bool { + let output = self.get_output_point(); + info!("Checking output bounds with cursor at ({}, {})", cursor_position.x, cursor_position.y); + if cursor_position.x > output.x - 5.0 && cursor_position.x < output.x + 5.0 { + debug!("Bound x match!"); + if cursor_position.y > output.y - 5.0 && cursor_position.y < output.y + 5.0 { + info!("Bounds match!"); + return true + } + } + false + } + + fn get_output_point(&self) -> Point { + return match self { + Curve::Connector{to, ..} => { + *to + }, + Curve::Mixer{output_point, ..} => { + *output_point + }, + } + } + fn draw_all(curves: &[Curve], frame: &mut Frame, theme: &Theme) { + let curves = Path::new(|p| { + for curve in curves { + match curve { + Curve::Connector{ from, to } => { + debug!("Drawing connector"); + p.move_to(*from); + // p.quadratic_curve_to(*control, *to); + let half_x_coord = from.x + (to.x - from.x)/2.0; + p.line_to(Point::new(half_x_coord, from.y)); + p.line_to(Point::new(half_x_coord, to.y)); + p.line_to(Point::new(to.x, to.y)); + let mut arrow_offset_x = -10.0; + let arrow_offset_y = 5.0; + if to.x < from.x { + arrow_offset_x *= -1.0; + } + p.line_to(Point::new(to.x + arrow_offset_x, to.y + arrow_offset_y)); + p.line_to(Point::new(to.x + arrow_offset_x, to.y - arrow_offset_y)); + p.line_to(Point::new(to.x, to.y)); + } + Curve::Mixer{at, input_point, output_point} => { + debug!("Drawing mixer."); + p.move_to(*at); + // p.rectangle(*at, Size::new(200.0, 200.0)); + let bottom_point = Point::new(at.x, at.y + 100.0); + let middle_point = Point::new(at.x + 100.0, at.y + 50.0); + p.line_to(bottom_point); + p.line_to(middle_point); + p.line_to(*at); + // Draw a circle for input connectors + p.move_to(*at); + p.circle(*input_point, 5.0); + // Another circle for output connectors + p.move_to(*at); + p.circle(*output_point, 5.0); + } + } + } + }); + + frame.stroke( + &curves, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); + } +} + +#[derive(Debug, Clone, Copy)] +enum Pending { + One { from: Point }, + // Two { from: Point, to: Point }, +} + +impl Pending { + fn draw( + &self, + renderer: &Renderer, + theme: &Theme, + bounds: Rectangle, + cursor: mouse::Cursor, + ) -> Geometry { + let mut frame = Frame::new(renderer, bounds.size()); + + if let Some(cursor_position) = cursor.position_in(bounds) { + match *self { + Pending::One { from } => { + let to = cursor_position; + let line = Path::new(|p| { + p.move_to(from); + // p.quadratic_curve_to(*control, *to); + let half_x_coord = from.x + (to.x - from.x)/2.0; + p.line_to(Point::new(half_x_coord, from.y)); + p.line_to(Point::new(half_x_coord, to.y)); + p.line_to(Point::new(to.x, to.y)); + + let mut arrow_offset_x = -10.0; + let arrow_offset_y = 5.0; + if to.x < from.x { + arrow_offset_x *= -1.0; + } + p.line_to(Point::new(to.x + arrow_offset_x, to.y + arrow_offset_y)); + p.line_to(Point::new(to.x + arrow_offset_x, to.y - arrow_offset_y)); + p.line_to(Point::new(to.x, to.y)); + }); + frame.stroke( + &line, + Stroke::default() + .with_width(2.0) + .with_color(theme.palette().text), + ); + } + }; + } + + frame.into_geometry() + } +} diff --git a/oscps-gui/src/main.rs b/oscps-gui/src/main.rs index 5f3a686..5fc3d3c 100644 --- a/oscps-gui/src/main.rs +++ b/oscps-gui/src/main.rs @@ -1,32 +1,259 @@ -#[derive(Default)] -struct Counter { - value: i8, +mod flowsheet; +mod style; + +use iced::widget::pane_grid::{self, PaneGrid}; +use iced::widget::{button, column, container, horizontal_space, hover, responsive, row, text}; +use iced::{Center, Element, Fill, Size, Theme}; + +use log::{info, debug}; + +pub fn main() -> iced::Result { + env_logger::init(); + info!("Starting application"); + iced::application("Open Source Chemical Process Simulator", MainWindow::update, MainWindow::view) + .theme(|_| Theme::CatppuccinMocha) + .antialiasing(true) + .centered() + .run() } + +struct MainWindow { + // theme: Theme, + panes: pane_grid::State, + focus: Option, + flowsheet: flowsheet::State, + curves: Vec, +} + #[derive(Debug, Clone, Copy)] -pub enum Message { - Increment, - Decrement, -} -use iced::widget::{button, row, text, Row}; -impl Counter { - pub fn view(&self) -> Row { - row![ - button("+").on_press(Message::Increment), - text(self.value).size(50), - button("-").on_press(Message::Decrement), - ] +enum Message { + AddCurve(flowsheet::Curve), + Clear, + PlaceComponent(flowsheet::BlockPlacement), + Clicked(pane_grid::Pane), + Dragged(pane_grid::DragEvent), + Resized(pane_grid::ResizeEvent), +} + +impl MainWindow { + + fn new() -> Self { + let (mut panes, pane) = pane_grid::State::new(Pane::new_canvas()); + panes.split(pane_grid::Axis::Horizontal, pane, Pane::new_selection()); + + MainWindow { + // theme: Theme::default(), + panes, + focus: None, + flowsheet: flowsheet::State::default(), + curves: Vec::default(), + } } - pub fn update(&mut self, message: Message) { + + fn update(&mut self, message: Message) { match message { - Message::Increment => { - self.value += 1; + Message::AddCurve(curve) => { + info!("Adding curve"); + self.curves.push(curve); + self.flowsheet.request_redraw(); + } + Message::Clear => { + self.flowsheet = flowsheet::State::default(); + self.curves.clear(); + } + // Default placement mode should be 'None' + Message::PlaceComponent(component) => { + match component { // TODO: Modify to do more work other than a simple assignment. + flowsheet::BlockPlacement::Default => { + info!("Setting to default placement mode."); + self.flowsheet.placement_mode = flowsheet::BlockPlacement::default(); + }, + flowsheet::BlockPlacement::Connector => { + info!("Setting to connector placement mode."); + self.flowsheet.placement_mode = flowsheet::BlockPlacement::Connector; + }, + flowsheet::BlockPlacement::Mixer => { + info!("Setting to mixer placement mode."); + self.flowsheet.placement_mode = flowsheet::BlockPlacement::Mixer; + }, + } + }, + Message::Clicked(pane) => { + self.focus = Some(pane); + info!("You clicked on a pane!") + }, + Message::Dragged(pane_grid::DragEvent::Dropped{ pane, target }) => { // pane, target + self.panes.drop(pane, target); + println!("You dragged a pane!") + }, + Message::Dragged(_) => { + println!("You dragged, but did not drop a pane!") + }, + Message::Resized(pane_grid::ResizeEvent { split, ratio } ) => { + self.panes.resize(split, ratio); + println!("You resized a pane!") + }, + } + } + + fn view(&self) -> Element { + let focus = self.focus; + let total_panes = self.panes.len(); + + let pane_grid = PaneGrid::new(&self.panes, |id, pane, _is_maximized| { + match pane { + Pane::Canvas + // { id: _, is_pinned: _} + => { + debug!("Found canvas!"); } - Message::Decrement => { - self.value -= 1; + Pane::UnitSelection => { + debug!("Found Selection!"); + return row![ + container( + button("Place Connector") + .style( + match self.flowsheet.placement_mode { + flowsheet::BlockPlacement::Connector => button::danger, + _ => button::secondary, + } + ) + .on_press( + match self.flowsheet.placement_mode { + flowsheet::BlockPlacement::Connector => Message::PlaceComponent(flowsheet::BlockPlacement::Default), + _ => Message::PlaceComponent(flowsheet::BlockPlacement::Connector) + } + ) + ), + container( + button("Place Mixer") + .style( + match self.flowsheet.placement_mode { + flowsheet::BlockPlacement::Mixer => button::danger, + _ => button::secondary, + } + ) + .on_press( + match self.flowsheet.placement_mode { + flowsheet::BlockPlacement::Mixer => Message::PlaceComponent(flowsheet::BlockPlacement::Default), + _ => Message::PlaceComponent(flowsheet::BlockPlacement::Mixer) + } + ) + ), + ].into() } } + let is_focused = focus == Some(id); + + let title = row![ + "Flowsheet", + ] + .spacing(5); + + let title_bar = pane_grid::TitleBar::new(title) + .padding(10) + .style(if is_focused { + style::title_bar_focused + } else { + style::title_bar_active + }); + + pane_grid::Content::new(responsive(move |size| { + view_content( + id, + total_panes, + false, + size, + hover( + self.flowsheet.view(&self.curves).map(Message::AddCurve), + if self.curves.is_empty() { + container(horizontal_space()) + } else { + container( + button("Clear") + .style(button::danger) + .on_press(Message::Clear), + ) + .padding(10) + .align_top(Fill) + }, + ), + + ) + })) + .title_bar(title_bar) + .style(if is_focused { + style::pane_focused + } else { + style::pane_active + }) + }) + .width(Fill) + .height(Fill) + .spacing(10) + .on_click(Message::Clicked) + .on_drag(Message::Dragged) + .on_resize(10, Message::Resized); + + container( + column![ + pane_grid, + ] + ) + .padding(20) + .into() + } +} + +impl Default for MainWindow { + fn default() -> Self { + MainWindow::new() + } +} + +#[derive(Clone,Copy,Default)] +enum Pane { + Canvas + // { + // id: usize, + // is_pinned: bool, + // } + , + #[default] + UnitSelection, +} + +impl Pane { + fn new_canvas( + // id: usize + ) -> Self { + Pane::Canvas + // { + // id, + // is_pinned: false, + // } + } + + fn new_selection() -> Self { + Pane::UnitSelection } + } -fn main() -> iced::Result { - iced::run("A cool counter", Counter::update, Counter::view) + +fn view_content<'a>( + _pane: pane_grid::Pane, + _total_panes: usize, + _is_pinned: bool, + size: Size, + flowsheet: Element<'a, Message>, +) -> Element<'a, Message> { + let content = + column![flowsheet, text!("{}x{}", size.width, size.height).size(24), ] // controls, + .spacing(10) + .align_x(Center); + + container(content) + .center_y(Fill) + .padding(5) + .into() } diff --git a/oscps-gui/src/style.rs b/oscps-gui/src/style.rs new file mode 100644 index 0000000..3cbdb64 --- /dev/null +++ b/oscps-gui/src/style.rs @@ -0,0 +1,50 @@ +use iced::widget::container; +use iced::{Border, Theme}; + +pub fn title_bar_active(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + container::Style { + text_color: Some(palette.background.strong.text), + background: Some(palette.background.strong.color.into()), + ..Default::default() + } +} + +pub fn title_bar_focused(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + container::Style { + text_color: Some(palette.primary.strong.text), + background: Some(palette.primary.strong.color.into()), + ..Default::default() + } +} + +pub fn pane_active(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + container::Style { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 2.0, + color: palette.background.strong.color, + ..Border::default() + }, + ..Default::default() + } +} + +pub fn pane_focused(theme: &Theme) -> container::Style { + let palette = theme.extended_palette(); + + container::Style { + background: Some(palette.background.weak.color.into()), + border: Border { + width: 2.0, + color: palette.primary.strong.color, + ..Border::default() + }, + ..Default::default() + } +} diff --git a/oscps-lib/src/blocks.rs b/oscps-lib/src/blocks.rs index c5cd30a..ff99f2c 100644 --- a/oscps-lib/src/blocks.rs +++ b/oscps-lib/src/blocks.rs @@ -1,39 +1,187 @@ //! # Blocks //! -//! This file contains traits implemented by various structs to represent +//! This file contains traits implemented by various structs to represent //! different unit operations. //! //! For example, if a block is a simple mixer, then it will implement the //! MassBalance trait but not the EnergyBalance. -use crate::connector; +use crate::connector::Stream; use once_cell::sync::Lazy; use uom::si::energy::joule; use uom::si::f64::Energy; use uom::si::f64::Mass; use uom::si::mass::kilogram; +/// # Block +/// +/// A trait that all blocks must implement. +/// TODO: In ASPEN, streams can be used to specify process inputs and outputs. +/// Instead, have special blocks that are 'source' and 'sink' blocks for +/// material entering and exiting the simulation. To make it more user friendly, +/// if a user attempts to run a simulation with stream that are not connected to +/// inputs or outputs, offer to automatically insert sources/sinks where the loose +/// ends are. While these special blocks will still have to implement this trait +/// (and thus implement unnecessary functions, such as the "connect_input" function +/// for a souce block, these functions can simply be dummy functions for this special case. +/// For safety, they can throw errors if called, but they should never be used. +pub trait Block { + /// Connect an input to a block. TODO: Have this function create the input stream and return a + /// reference to it. Then use that stream reference to connect an output. + fn connect_input(&mut self, stream: &mut Stream) -> Result<(), &str>; + /// Disconnect an input to a block + fn disconnect_input(&mut self, stream: &mut Stream) -> Result<(), &str>; + /// Connect an output to a block + fn connect_output(&mut self, stream: &mut Stream) -> Result<(), &str>; + /// Disconnect an output to a block + fn disconnect_output(&mut self, stream: &mut Stream) -> Result<(), &str>; + // TODO: Add additional functions that all Blocks should implement +} + +/// # Separator +/// +/// A Separator block that allows components of a stream to be separated. +/// Allows for a single input and an arbitrary number of outputs. +struct Separator { + id: u64, + input: Option, // An option is used in case there is no input stream + outputs: Vec, // An empty vec can represent no outputs, no need for Option>> + // TODO: Add additional fields that controls how components are separated +} + +impl Separator { + fn new(id: u64) -> Self { + Separator { + id, + input: None, + outputs: Vec::new(), + } + } +} + +#[allow(dead_code)] +/// # Mixer +/// +/// A block used for simple stream mixing operations. Spacial information +/// is not stored in the case that non-gui applications use this backend. +pub struct Mixer { + /// The ID of the block. + pub id: u64, + /// Set of inlet streams for the mixer + pub inputs: Vec>, + /// outlet stream for the mixer block + pub output: Option>, +} + +#[allow(dead_code)] +/// Implementations of the mixer block. +impl Mixer { + /// Create a new mixer block. TODO: Figure out importance of lifetimes + pub fn new<'a>(id: u64) -> Mixer { + Mixer { + id, + inputs: Vec::new(), + output: None, + } + } + + // TODO: Uncomment once desired base functionality is achieved + // /// Execute the mixer block (calculate balances, output streams, etc) + // /// This function still needs to be implemented + // pub fn execute_block(&mut self) { + // self.outlet_stream = Some(connector::Stream { + // s_id: String::from("Mass_Outlet"), + // thermo: None, + // from_block: String::from("M1"), + // to_block: String::from("M2") + // // m_flow_total: self.compute_total_outlet_mass_flow().unwrap(), + // }); + // // self.outlet_stream_energy = Some(connector::Econnector { + // // e_conn_id: String::from("Energy Outlet"), + // // energy_flow_total: self.compute_outlet_energy_flows().unwrap(), + // // }); + // } + + // /// This private method will compute the outlet mass flows for the mixer block + // /// + // /// # Returns + // /// + // /// A Mass quantity (uom object) that holds the outlet mass flow + // fn compute_total_outlet_mass_flow(&self) -> Option { + // // TODO: steps to implement function: + // // Need to loop through each of the connector structures and add up the mass flows + // // During this process, need to make sure that all the mass flows are in the same units + // // Use the UOM package to help with this part... + // let mut mass_flow_sum: f64 = 0.0; + + // for s in self.inlet_streams.iter() { + // mass_flow_sum += s.thermo.as_ref().unwrap().total_mass(); + // } + // Some(mass_flow_sum) + // } + + // /// Determines the total energy flowing through the block + // fn compute_outlet_energy_flows(&self) -> Option { + // let mut energy_flow_sum: f64 = 0.0; + + // for s in self.inlet_streams.iter() { + // energy_flow_sum += s.thermo.as_ref().unwrap().enthalpy(); + // } + // Some(energy_flow_sum) + // } + + // /// Determines the phase fractions of the output using thermodynamics. + // /// TODO: Implement this function + // fn compute_outlet_phase_fractions(&self) {} + + // /// Computes the outlet temperature of the mixer (assumes no chemical + // /// reactions) TODO: Implement this function + // fn compute_outlet_temperature(&self) {} + + // /// Computes the mixer outlet pressure. + // /// TODO: Implement this function + // fn compute_outlet_pressure(&self) {} +} + +impl Block for Mixer { + fn connect_input<'a>(&mut self, _stream: &mut Stream) -> Result<(), &'static str> { + // TODO: Figure out how to store references to streams + // self.inputs.push(stream); + Ok(()) + } + + fn disconnect_input(&mut self, _stream: &mut Stream) -> Result<(), &'static str> { + Ok(()) + } + + fn connect_output(&mut self, _stream: &mut Stream) -> Result<(), &'static str> { + Ok(()) + } + + fn disconnect_output(&mut self, _stream: &mut Stream) -> Result<(), &'static str> { + Ok(()) + } +} + #[allow(dead_code)] -/// Minimum error allowed for energy difference. +/// Minimum error allowed for energy difference. /// TODO: Change this to a relative scale instead of an absolute scale. -pub static TOLERENCE_ENERGY: Lazy = - Lazy::new(|| Energy::new::(5.0)); +pub static TOLERENCE_ENERGY: Lazy = Lazy::new(|| Energy::new::(5.0)); #[allow(dead_code)] -/// Minimum error allowed for mass difference. +/// Minimum error allowed for mass difference. /// TODO: Change this to a relative scale instead of an absolute scale. -pub static TOLERENCE_MASS: Lazy = - Lazy::new(|| Mass::new::(5.0)); +pub static TOLERENCE_MASS: Lazy = Lazy::new(|| Mass::new::(5.0)); #[allow(dead_code)] /// # MassBalance /// /// Trait for ensuring the overall mass balance is maintained in a flowsheet. /// -/// This trait can be implemented by any block that needs to ensure mass +/// This trait can be implemented by any block that needs to ensure mass /// conservation. pub trait MassBalance { - /// Perform a mass balance check on object by comparing inlet and outlet + /// Perform a mass balance check on object by comparing inlet and outlet /// mass. TODO: Compare mass flow rates, not mass and check for relative /// error instead of absolute, perhaps error should be less than 1e-6 /// fraction of the total inlet mass. This can be an adjustable parameter. @@ -49,16 +197,15 @@ pub trait MassBalance { #[allow(dead_code)] /// # EnergyBalance /// -/// This trait ensures that blocks in the flowsheet adhere to energy +/// This trait ensures that blocks in the flowsheet adhere to energy /// conservation principles. pub trait EnergyBalance { /// Perform an energy balance on a block. Checks all input and output - /// streams and ensures that energy stays the same. TODO: Ensure that + /// streams and ensures that energy stays the same. TODO: Ensure that /// energy loss is accounted for. For example, a mixer may not be entirely - /// adiabatic, and therefor some energy will be lost to the environment. + /// adiabatic, and therefor some energy will be lost to the environment. /// Also implement changes in issue #19. - fn energy_balance_check(&self, energy_in: Energy, energy_out: Energy) -> - bool { + fn energy_balance_check(&self, energy_in: Energy, energy_out: Energy) -> bool { let energy_in_joules = energy_in.get::(); let energy_out_joules = energy_out.get::(); let energy_difference = energy_in_joules - energy_out_joules; @@ -66,191 +213,96 @@ pub trait EnergyBalance { } } -#[allow(dead_code)] -/// # Mixer -/// -/// A block used for simple stream mixing operations. -pub struct Mixer { - /// The ID of the block. - pub block_id: String, - /// The x-coordiante on a flowsheet of the block. - pub x_cord: i32, - /// The y-coordinate on a flowsheet of the block. - pub y_cord: i32, - /// All mass input streams for the block. - pub input_streams_mass: Vec, - /// All energy input streams for the block. - pub input_streams_energy: Vec, - /// All mass output streams for the block. - pub outlet_stream_mass: Option, - /// All energy output streams for the block - pub outlet_stream_energy: Option, -} - /// Applying mass balance trait to Mixer Block impl MassBalance for Mixer {} /// Applying the energy balance trait to the Mixer Block impl EnergyBalance for Mixer {} -#[allow(dead_code)] -/// Implementations of the mixer block. -impl Mixer { - /// Create a new mixer block. - pub fn new( - id: String, - x_cord: i32, - y_cord: i32, - in_streams_mass: Vec, - in_streams_energy: Vec, - ) -> Mixer { - Mixer { - block_id: id, - x_cord, - y_cord, - input_streams_mass: in_streams_mass, - input_streams_energy: in_streams_energy, - outlet_stream_mass: None, - outlet_stream_energy: None, - } - } - - /// Execute the mixer block (calculate balances, output streams, etc) - /// TODO: This block should calculate the new state of external connectors. - pub fn execute_block(&mut self) { - self.outlet_stream_mass = Some(connector::Mconnector { - m_conn_id: String::from("Mass_Outlet"), - m_flow_total: self.compute_total_outlet_mass_flow().unwrap(), - }); - self.outlet_stream_energy = Some(connector::Econnector { - e_conn_id: String::from("Energy Outlet"), - energy_flow_total: self.compute_outlet_energy_flows().unwrap(), - }); +/// Block implements Clone +impl Clone for Box { + fn clone(&self) -> Self { + todo!(); } - - /// This private method will compute the outlet mass flows for the mixer block - /// - /// # Returns - /// - /// A Mass quantity (uom object) that holds the outlet mass flow - fn compute_total_outlet_mass_flow(&self) -> Option { - // TODO: steps to implement function: - // Need to loop through each of the connector structures and add up the mass flows - // During this process, need to make sure that all the mass flows are in the same units - // Use the UOM package to help with this part... - let mut mass_flow_sum: f64 = 0.0; - - for stream in &self.input_streams_mass { - mass_flow_sum += stream.m_flow_total; - } - Some(mass_flow_sum) - } - - /// Determines the total energy flowing through the block - fn compute_outlet_energy_flows(&self) -> Option { - let mut energy_flow_sum: f64 = 0.0; - - for stream in &self.input_streams_energy { - energy_flow_sum += stream.energy_flow_total; - } - Some(energy_flow_sum) - } - - /// Determines the phase fractions of the output using thermodynamics. - /// TODO: Implement this function - fn compute_outlet_phase_fractions(&self) {} - - /// Computes the outlet temperature of the mixer (assumes no chemical - /// reactions) TODO: Implement this function - fn compute_outlet_temperature(&self) {} - - /// Computes the mixer outlet pressure. - /// TODO: Implement this function - fn compute_outlet_pressure(&self) {} } /// # Block Tests -/// +/// /// The following module holds all the unit test cases for the blocks module #[cfg(test)] mod block_tests { - use crate::connector::{Econnector, Mconnector}; - - use super::*; - use uom::si::energy::kilojoule; - use uom::si::f64::Energy; - use uom::si::mass::pound; - - #[test] - /// checks whether the mass balance check function was implemented properly - fn test_mass_balance_check_steady_state_for_mixer() { - // here you will need to check that the mass into the mixer = mass out of mixer - - let mixer_test_obj = Mixer { - block_id: String::from("Test Mixer"), - x_cord: 0, - y_cord: 0, - input_streams_mass: Vec::new(), - input_streams_energy: Vec::new(), - outlet_stream_mass: None, - outlet_stream_energy: None, - }; - let mass_in = Mass::new::(100.0); - let mass_out = Mass::new::(95.0); - assert!(mixer_test_obj.mass_balance_check(mass_in, mass_out)); - } + // use crate::connector::Stream; - #[test] - /// checks if the 'energy_balance_check' function was implemented properly - fn test_energy_balance_check_steady_state_for_mixer() { - // energy into mixer = energy out of mixer - let mixer_test_obj = Mixer { - block_id: String::from("Test Mixer"), - x_cord: 0, - y_cord: 0, - input_streams_mass: Vec::new(), - input_streams_energy: Vec::new(), - outlet_stream_mass: None, - outlet_stream_energy: None, - }; - let energy_in = Energy::new::(10.0); - let energy_out = Energy::new::(95.0); - assert!(mixer_test_obj.energy_balance_check(energy_in, energy_out)); - } + // use super::*; + // use uom::si::energy::kilojoule; + // use uom::si::f64::Energy; + // use uom::si::mass::pound; - #[test] - /// checking functionality of 'compute_total_outlet_mass_flow' - fn test_compute_total_outlet_mass_flow() { - let in_streams_mass = vec![ - Mconnector { - m_conn_id: String::from("Mass1"), - m_flow_total: 3.0, - }, - Mconnector { - m_conn_id: String::from("Mass2"), - m_flow_total: 7.0, - }, - ]; - let mixer = Mixer::new(String::from("Mixer3"), 0, 0, in_streams_mass, vec![]); - - assert_eq!(mixer.compute_total_outlet_mass_flow(), Some(10.0)); - } + // #[test] + // /// checks whether the mass balance check function was implemented properly + // fn test_mass_balance_check_steady_state_for_mixer() { + // // here you will need to check that the mass into the mixer = mass out of mixer - #[test] - /// checking functionality of 'compute_outlet_energy_flows' - fn test_compute_outlet_energy_flows() { - let in_streams_energy = vec![ - Econnector { - e_conn_id: String::from("Energy1"), - energy_flow_total: 100.0, - }, - Econnector { - e_conn_id: String::from("Energy2"), - energy_flow_total: 200.0, - }, - ]; - let mixer = Mixer::new(String::from("Mixer5"), 0, 0, vec![], in_streams_energy); - - assert_eq!(mixer.compute_outlet_energy_flows(), Some(300.0)); - } + // let mixer_test_obj = Mixer { + // block_id: String::from("Test Mixer"), + // y_cord: 0, + // inlet_streams: Vec::new(), + // outlet_stream: None, + // }; + // let mass_in = Mass::new::(100.0); + // let mass_out = Mass::new::(95.0); + // assert!(mixer_test_obj.mass_balance_check(mass_in, mass_out)); + // } + + // #[test] + // /// checks if the 'energy_balance_check' function was implemented properly + // fn test_energy_balance_check_steady_state_for_mixer() { + // // energy into mixer = energy out of mixer + // let mixer_test_obj = Mixer { + // block_id: String::from("Test Mixer"), + // x_cord: 0, + // y_cord: 0, + // inlet_streams: Vec::new(), + // outlet_stream: None, + // }; + // let energy_in = Energy::new::(10.0); + // let energy_out = Energy::new::(95.0); + // assert!(mixer_test_obj.energy_balance_check(energy_in, energy_out)); + // } + + // #[test] + // checking functionality of 'compute_total_outlet_mass_flow' + // fn test_compute_total_outlet_mass_flow() { + // let in_streams_mass = vec![ + // Mconnector { + // m_conn_id: String::from("Mass1"), + // m_flow_total: 3.0, + // }, + // Mconnector { + // m_conn_id: String::from("Mass2"), + // m_flow_total: 7.0, + // }, + // ]; + // let mixer = Mixer::new(String::from("Mixer3"), 0, 0, in_streams_mass, vec![]); + + // assert_eq!(mixer.compute_total_outlet_mass_flow(), Some(10.0)); + // } + + // #[test] + // /// checking functionality of 'compute_outlet_energy_flows' + // fn test_compute_outlet_energy_flows() { + // let in_streams_energy = vec![ + // Econnector { + // e_conn_id: String::from("Energy1"), + // energy_flow_total: 100.0, + // }, + // Econnector { + // e_conn_id: String::from("Energy2"), + // energy_flow_total: 200.0, + // }, + // ]; + // let mixer = Mixer::new(String::from("Mixer5"), 0, 0, vec![], in_streams_energy); + + // assert_eq!(mixer.compute_outlet_energy_flows(), Some(300.0)); + // } } diff --git a/oscps-lib/src/component.rs b/oscps-lib/src/component.rs index 48e6ecf..a4581cd 100644 --- a/oscps-lib/src/component.rs +++ b/oscps-lib/src/component.rs @@ -90,6 +90,14 @@ pub struct ChemicalProperties { pub critical_pressure: f64, // Pa /// Acentric factor of a compound. pub acentric_factor: f64, + ///Heat capacity Coefficient A + pub const_a: f64, + ///Heat capacity Coefficient B + pub const_b: f64, + ///Heat capacity Coefficient C + pub const_c: f64, + ///Heat capacity Coefficient D + pub const_d: f64 } /// Implementation of the ChemicalProperties struct. @@ -102,6 +110,10 @@ impl ChemicalProperties { critical_temp: 0.0, // K critical_pressure: 0.0, // Pa acentric_factor: 0.0, + const_a: 0.0, + const_b: 0.0, + const_c: 0.0, + const_d: 0.0 }) } } diff --git a/oscps-lib/src/connector.rs b/oscps-lib/src/connector.rs index 9b37b80..fa67621 100644 --- a/oscps-lib/src/connector.rs +++ b/oscps-lib/src/connector.rs @@ -1,54 +1,37 @@ //! # Connector //! -//! Two types of connectors represent different energy types of streams -//! One is a mass connector, Mconnector, which represents mass flow rates -//! -//! The other is Econnector, which represents energy flow rates. -#[allow(dead_code)] -/// A connector for storing mass information. This includes an ID and a -/// total mass flow rate. -/// -/// TODO: These should be consolidated into a single connector. -pub struct Mconnector { - /// Mass connector ID - pub m_conn_id: String, - /// Total mass flow rate - pub m_flow_total: f64, -} +use crate::thermodynamics::ThermoState; -#[allow(dead_code)] -/// Functions implemented on Mconnectors. -impl Mconnector { - /// Constructor for a connector. - pub fn new(id: String) -> Mconnector { - Mconnector { - m_conn_id: id, - m_flow_total: 0.0, - } - } +/// # Stream +/// +/// Struct to hold stream information +pub struct Stream { + // TODO: IDs must be unique within the flowsheet. Consider using integers + // as IDs and having a separate field for the name of a connector. Adopt + // a similar scheme for blocks. + /// ID of the stream. + pub s_id : String, + /// Instance of ThermoState struct that holds thermodynamic information. + pub thermo : Option, + // TODO: Change these from strings to integers, or better yet, + // references to the source and destination blocks, to minimize + // computation time spent on looking for sources and destinations. + /// ID of source block + pub from_block : String, + /// ID of destination block + pub to_block : String } -#[allow(dead_code)] -/// A connector for storing energy information. This includes an ID and a -/// total energy flow rate. -/// -/// TODO: These should be consolidated into a single connector. -pub struct Econnector { - /// Energy connector ID. - pub e_conn_id: String, - /// Total energy flow rate. - pub energy_flow_total: f64, -} -#[allow(dead_code)] -/// Functions implemented on Econnectors. -impl Econnector { - /// Constructor for a connector. - pub fn new(id: String) -> Econnector { - Econnector { - e_conn_id: id, - energy_flow_total: 0.0, +impl Stream { + /// Constructor for 'Stream' struct + pub fn new(id: String, from_blk_id : String, to_blk_id : String) -> Stream { + Stream { + s_id : id, + thermo : None, + from_block : from_blk_id, + to_block : to_blk_id } } } diff --git a/oscps-lib/src/lib.rs b/oscps-lib/src/lib.rs index 6a5cceb..e81652e 100644 --- a/oscps-lib/src/lib.rs +++ b/oscps-lib/src/lib.rs @@ -11,7 +11,3 @@ pub mod connector; pub mod simulation; pub mod thermodynamics; -/// An example function. This will be removed in the final release. -pub fn hello() -> String { - "Hello, world!".to_string() -} diff --git a/oscps-lib/src/thermodynamics.rs b/oscps-lib/src/thermodynamics.rs index a1bd05b..47e969c 100644 --- a/oscps-lib/src/thermodynamics.rs +++ b/oscps-lib/src/thermodynamics.rs @@ -2,16 +2,20 @@ //! //! This module will hold all the functions related to calculating //! themrodynamic properties for the blocks and chemical species. -//! -//! TODO: All public items, including struct members, must be documented. Placeholder -//! documentation is in place, but more descriptive documentation should be -//! implemented in the future. +///Importing Ideal Gas Package +pub mod ideal_gas_package; +///Importing SRK Package +pub mod srk_package; use crate::component::Chemical; + use uom::si::f64::*; -use uom::si::mass::kilogram; -use uom::si::pressure::pascal; -use uom::si::thermodynamic_temperature::kelvin; +use uom::si::mass; +use uom::si::pressure; +use uom::si::thermodynamic_temperature; +use uom::si::energy; +use uom::si::amount_of_substance; + #[allow(dead_code)] /// Struct for storing physical constants for thermodynamics. @@ -27,208 +31,243 @@ pub enum ThermodynamicConstants { AvogadroNumber, // N_A } -#[allow(dead_code)] -/// Enum for representing different types of thermodynamic constant values -pub enum ConstantValue { - /// Pressure value - Pressure(Pressure), - /// Temperature value - Temperature(ThermodynamicTemperature), - /// Dimensionless value - Dimensionless(f64), -} - #[allow(dead_code)] /// Implements values of thermodynamic constants. impl ThermodynamicConstants { /// Returns the value of the thermodynamic constant with its appropriate type. - pub fn value(&self) -> ConstantValue { + pub fn value(&self) -> Box { match self { ThermodynamicConstants::UniversalGasConstant => { - ConstantValue::Pressure(Pressure::new::(8.314462618)) - } + let r = 8.314462618; + let constant = Energy::new::(r) / (ThermodynamicTemperature::new::(1.0)* AmountOfSubstance::new::(1.0)); + Box::new(constant) + }, ThermodynamicConstants::StandardTemperature => { - ConstantValue::Temperature(ThermodynamicTemperature::new::(273.15)) + Box::new(ThermodynamicTemperature::new::(273.15)) } ThermodynamicConstants::StandardPressure => { - ConstantValue::Pressure(Pressure::new::(101_325.0)) - } - ThermodynamicConstants::AvogadroNumber => ConstantValue::Dimensionless(6.02214076e23), + Box::new(Pressure::new::(101_325.0)) + }, + ThermodynamicConstants::AvogadroNumber => Box::new(6.02214076e23), //Units: particles/mole } } } #[allow(dead_code)] +/// Species list +pub struct ComponentData { + /// Chemical species + pub chemical_species: Chemical, // will contain intrinsic properties of species + /// Mass quantity + pub mass_quantity: Mass, + /// Molar quantity + pub molar_quantity: AmountOfSubstance, + ///volumetric quantity + pub vol_quantity: Volume, + /// partial pressure + pub partial_pressure : Pressure, +} + +#[allow(dead_code)] +/// # ThermoState /// Returns a thermodynamic state, including pressure, temperature, and /// mole fractions. +/// This struct will be used for streams in the flow diagram pub struct ThermoState { /// Pressure of the state. - pub pressure: Pressure, // Pressure in Pascals + pub pressure: Option, // pressure /// Temperature of the state. - pub temperature: ThermodynamicTemperature, // Temperature in Kelvin + pub temperature: Option, // temperature /// List of mole fractions. - pub mass_list: Vec, // Mole fractions, typically unitless + pub mass_list: Vec,//Information about each component within stream + /// Total Mass + pub total_mass : Option, // total mass in stream + /// Total Moles + pub total_mol : Option, // total moles in stream + /// Total Volume + pub total_volume : Option, // total volume in stream + ///Thermo Package + pub thermodynamic_package : Option> // thermodynamics package } -#[allow(dead_code)] -/// Species list -pub struct SpeciesListPair { - /// Chemical species - pub chemical_species: Chemical, - /// Mass quantity - pub mass_quantity: Mass, -} #[allow(dead_code)] /// Implementation of ThermoState -/// This struct holds the functionality to perform thermodynamic calculations for a stream or for -/// an individual species +/// This struct holds the functionality to perform thermodynamic calculations for streams impl ThermoState { /// Constructor for creating a ThermoState - pub fn new( - pressure: f64, // in Pascals - temperature: f64, // in Kelvin - mass_list: Vec, - ) -> Self { + pub fn new() -> Self { ThermoState { - pressure: Pressure::new::(pressure), - temperature: ThermodynamicTemperature::new::(temperature), - mass_list, + pressure : None, + temperature : None, + mass_list : vec![], + total_mass : None, + total_mol : None, + total_volume : None, + thermodynamic_package : None } } - - /// Determine mass fraction - pub fn mass_frac(&self, species: &Chemical) -> Option { - let mut total_mass = 0.0; - let mut component_mass = 0.0; - + /// this function will return the total mass for an individual stream + fn calc_total_mass(&mut self) -> Mass { + let mut mass_sum = 0.0; for chem in &self.mass_list { - total_mass += chem.mass_quantity.get::(); - - if let Some(cids) = Some(chem.chemical_species.pubchem_obj.cids().unwrap()[0]) { - if cids == species.pubchem_obj.cids().unwrap_or_default()[0] { - component_mass = chem.mass_quantity.get::(); - } - } + mass_sum += chem.mass_quantity.get::(); } - - match component_mass { - 0.0 => None, - _ => Some(component_mass / total_mass), + self.total_mass = Some(Mass::new::(mass_sum)); + + self.total_mass.unwrap() + } + /// this function will return the total moles for an individual stream + fn calc_total_moles(&mut self) -> AmountOfSubstance { + let mut mole_sum = 0.0; + for chem in &self.mass_list { + mole_sum += chem.molar_quantity.get::(); } + self.total_mol = Some(AmountOfSubstance::new::(mole_sum)); + + self.total_mol.unwrap() } +} - /// Determine ideal gas pressure - fn ideal_gas_pressure(&self, n: f64, t: f64, v: f64) -> f64 { - const R: f64 = 8.314; // J/(mol·K) - (n * R * t) / v - } +///Thermodynamic Packages. +/// +///#ThermoPackage +///Will be a common trait for all the thermodynamic packages +///Will include functions common to thermodynamic packages +///Will also enable to user to switch between thermodynamic packages within the ThermoState struct +///(the thermodynamic packages will be structs) +pub trait ThermoPackage{ + ///Calculating the Enthalpy + fn enthalpy(&self) -> MolarEnergy; + ///Calculating the Entropy + fn entropy(&self) -> MolarHeatCapacity; + ///Calculate pressure + fn pressure(&self) -> Pressure; + ///Calculate volume + fn volume(&self) -> Volume; + ///Calculate temperature + fn temperature(&self) -> ThermodynamicTemperature; + ///Calculate vapor fractions + fn vapor_fraction(&self) -> Ratio; + ///Calculate heat capacity + fn heat_capacity_const_pressure(&self) -> MolarHeatCapacity; + ///Calculate internal temperature + fn internal_energy(&self) -> MolarEnergy; + ///Calculate gibbs free energy + fn gibbs_free_energy(&self) -> Energy; } + + + #[cfg(test)] mod thermo_tests { - use super::*; - use crate::component::{Chemical, ChemicalProperties}; - use uom::si::mass::kilogram; - use uom::si::pressure::pascal; - use uom::si::thermodynamic_temperature::kelvin; - use std::{thread,time::Duration}; - - #[test] - ///Test case generates an instance of the 'ThermoState' struct - fn test_create_thermo_state() { - // Create some test data for ThermoMoleFrac (mole fractions) - let water = Chemical { - pubchem_obj: pubchem::Compound::new(962), - properties: ChemicalProperties { - molar_mass: 0.01801528, // kg/mol for water - critical_temp: 647.1, // K - critical_pressure: 2206.0, // Pa - acentric_factor: 0.344, // example - }, - }; - thread::sleep(Duration::from_secs(10)); - let water_mass = Mass::new::(2.0); - let water_species_pair = SpeciesListPair { - chemical_species: water, - mass_quantity: water_mass, - }; - - // Create ThermoState - let thermo_state = ThermoState::new( - 101325.0, // pressure in Pascals (1 atm) - 298.15, // temperature in Kelvin (25°C) - vec![water_species_pair], // Example with one chemical - ); - - // Validate ThermoState - assert_eq!(thermo_state.pressure.get::(), 101325.0); - assert_eq!(thermo_state.temperature.get::(), 298.15); - assert_eq!(thermo_state.mass_list.len(), 1); // Should contain one mole fraction entry + // use super::*; + // use crate::component::{Chemical, ChemicalProperties}; + // use uom::si::mass::kilogram; + // use uom::si::pressure::pascal; + // use uom::si::thermodynamic_temperature::kelvin; + // use std::{thread,time::Duration}; - + // #[test] + // ///Test case generates an instance of the 'ThermoState' struct + // fn test_create_thermo_state() { + // // Create some test data for ThermoMoleFrac (mole fractions) + // let water = Chemical { + // pubchem_obj: pubchem::Compound::new(962), + // properties: ChemicalProperties { + // molar_mass: 0.01801528, // kg/mol for water + // critical_temp: 647.1, // K + // critical_pressure: 2206.0, // Pa + // acentric_factor: 0.344, // example + // }, + // }; + // thread::sleep(Duration::from_secs(10)); + // let water_mass = Mass::new::(2.0); + // let water_species_pair = SpeciesQuantityPair { + // chemical_species: water, + // mass_quantity: water_mass, + // const_a: 1.0, + // const_b: 1.0, + // const_c: 1.0, + // const_d: 0.0 + // }; - // Check that the mole fraction's chemical is correctly set - assert_eq!( - thermo_state.mass_list[0] - .chemical_species - .get_pubchem_obj() - .cids() - .unwrap()[0], - 962 - ); - } + // // Create ThermoState + // let thermo_state = ThermoState::new( + // 101325.0, // pressure in Pascals (1 atm) + // 298.15, // temperature in Kelvin (25°C) + // vec![water_species_pair], // Example with one chemical + // ); - #[test] - ///Tests the mass fraction function within the 'ThermoState struct' - fn test_mass_fraction_calculation() { - let water = Chemical { - pubchem_obj: pubchem::Compound::new(962), - properties: ChemicalProperties { - molar_mass: 0.01801528, // kg/mol for water - critical_temp: 647.1, // K - critical_pressure: 2206.0, // Pa - acentric_factor: 0.344, // example - }, - }; - thread::sleep(Duration::from_secs(10)); - - let anisdine = Chemical { - pubchem_obj: pubchem::Compound::new(7732), - properties: ChemicalProperties { - molar_mass: 123.155, // g/mol, converting to kg/mol = 123.155 / 1000 - critical_temp: 592.0, // K (approximated) - critical_pressure: 2.6e6, // Pa (approximated) - acentric_factor: 0.24, // (approximated) - }, - }; - thread::sleep(Duration::from_secs(10)); - - let water_mass = Mass::new::(2.0); - let water_species_pair = SpeciesListPair { - chemical_species: water, - mass_quantity: water_mass, - }; - - let anisidine_mass = Mass::new::(8.0); - let anisidine_species_pair = SpeciesListPair { - chemical_species: anisdine, - mass_quantity: anisidine_mass, - }; - - let therm_obj = ThermoState::new( - 101325.0, - 298.15, - vec![water_species_pair, anisidine_species_pair], - ); - - let mass_fraction = therm_obj - .mass_frac(&therm_obj.mass_list[0].chemical_species) - .unwrap(); - - assert!( - (mass_fraction - 0.2).abs() < 1e-6, - "Mole fraction calculation failed" - ); // Should be 0.2 - } + // // Validate ThermoState + // assert_eq!(thermo_state.pressure.get::(), 101325.0); + // assert_eq!(thermo_state.temperature.get::(), 298.15); + // assert_eq!(thermo_state.mass_list.len(), 1); // Should contain one mole fraction entry + + // + + // // Check that the mole fraction's chemical is correctly set + // assert_eq!( + // thermo_state.mass_list[0] + // .chemical_species + // .get_pubchem_obj() + // .cids() + // .unwrap()[0], + // 962 + // ); + // } + + // #[test] + // ///Tests the mass fraction function within the 'ThermoState struct' + // fn test_mass_fraction_calculation() { + // let water = Chemical { + // pubchem_obj: pubchem::Compound::new(962), + // properties: ChemicalProperties { + // molar_mass: 0.01801528, // kg/mol for water + // critical_temp: 647.1, // K + // critical_pressure: 2206.0, // Pa + // acentric_factor: 0.344, // example + // }, + // }; + // thread::sleep(Duration::from_secs(10)); + + // let anisdine = Chemical { + // pubchem_obj: pubchem::Compound::new(7732), + // properties: ChemicalProperties { + // molar_mass: 123.155, // g/mol, converting to kg/mol = 123.155 / 1000 + // critical_temp: 592.0, // K (approximated) + // critical_pressure: 2.6e6, // Pa (approximated) + // acentric_factor: 0.24, // (approximated) + // }, + // }; + // thread::sleep(Duration::from_secs(10)); + // + // let water_mass = Mass::new::(2.0); + // let water_species_pair = SpeciesQuantityPair { + // chemical_species: water, + // mass_quantity: water_mass, + // }; + + // let anisidine_mass = Mass::new::(8.0); + // let anisidine_species_pair = SpeciesQuantityPair { + // chemical_species: anisdine, + // mass_quantity: anisidine_mass, + // }; + + // let therm_obj = ThermoState::new( + // 101325.0, + // 298.15, + // vec![water_species_pair, anisidine_species_pair], + // ); + + // let mass_fraction = therm_obj + // .mass_frac(&therm_obj.mass_list[0].chemical_species) + // .unwrap(); + + // assert!( + // (mass_fraction - 0.2).abs() < 1e-6, + // "Mole fraction calculation failed" + // ); // Should be 0.2 + // } } diff --git a/oscps-lib/src/thermodynamics/ideal_gas_package.rs b/oscps-lib/src/thermodynamics/ideal_gas_package.rs new file mode 100644 index 0000000..d399335 --- /dev/null +++ b/oscps-lib/src/thermodynamics/ideal_gas_package.rs @@ -0,0 +1,179 @@ +///#IdealGasPackage +/// +///Will contain equations related to ideal gases + + +use crate::thermodynamics::*; +use std::sync::Arc; +use uom::si::f64::*; +use uom::si::molar_energy; +use uom::si::molar_heat_capacity; +use uom::si::pressure; +use uom::si::thermodynamic_temperature; +use uom::si::energy; +use uom::si::amount_of_substance; +use uom::si::volume; +use uom::si::ratio; + +///Creating the ideal gas thermodynamics package +pub struct IdealGasPackage { + ///Temperature + pub temperature : Arc, + /// Pressure + pub pressure : Arc, + ///List of Species + pub species_list : Vec>, + /// Mass + pub total_mass : Arc, + /// Volume + pub total_vol : Arc, + /// Moles + pub total_mol : Arc +} +///Implementing functions specific to the IdealGasPackage +impl IdealGasPackage { + ///Constructor + pub fn new( + temperature: Arc, + pressure : Arc, + species_list : Vec>, + total_mass : Arc, + total_vol : Arc, + total_mol : Arc) -> Self { + IdealGasPackage { + temperature, + pressure, + species_list, + total_mass, + total_vol, + total_mol + } + } +} +/// Implementing the ThermoPackage trait for the IdealGasPackage +impl ThermoPackage for IdealGasPackage { + ///Calculating enthalpy + // Need to run a for loop where I calculate the enthalpy of each species and then add it to + // the variable 'total_enthalpy' + // ASSUMPTIONS CURRENTLY MADE: + // No enthalpy from phase change + // when working with gases, assume that they are ideal gases + // Tref = 298 K & Pref = 101.325 kPa + // Href = 0 + fn enthalpy(&self) -> MolarEnergy { + let mut total_enthalpy = 0.0; + let t_ref = 298.15; //reference temperature + let h_ref = 0.0; //Reference enthalpy + let mut cp_ref; + let mut cp_t; + let r = ThermodynamicConstants::UniversalGasConstant.value().downcast::().unwrap(); + + for chem_object in &self.species_list { + let chem = &(*chem_object).chemical_species.properties; + if chem.const_c != 0.0 { + cp_ref = chem.const_a * t_ref + (1.0 / 2.0) * (chem.const_b / (10.0f64.powf(3.0))) * t_ref.powi(2); + cp_t = chem.const_a * self.temperature.get::() + (1.0 / 2.0) * (chem.const_b / (10.0f64.powf(3.0))) * self.temperature.get::().powf(2.0) + (1.0 / 3.0) * (chem.const_c / (10.0f64.powf(6.0))) * self.temperature.get::().powf(3.0); + } + else{ + cp_ref = chem.const_a * t_ref + (1.0 / 2.0) * (chem.const_b / (10.0f64.powf(3.0))) * t_ref.powi(2) + (-1.0) * (chem.const_d / (10.0f64.powf(-5.0))) * t_ref.powi(-1); + cp_t = chem.const_a * self.temperature.get::() + (1.0 / 2.0) * (chem.const_b / (10.0f64.powf(3.0))) * self.temperature.get::().powf(2.0) + (-1.0) * (chem.const_d / (10.0f64.powf(-5.0))) * self.temperature.get::().powf(-1.0); + } + let species_enthalpy = (chem_object.molar_quantity.get::()/self.total_mol.get::())*(h_ref + (cp_t - cp_ref)* r.get::()); + total_enthalpy += species_enthalpy; + } + + MolarEnergy::new::(total_enthalpy) + } + /// Determine ideal gas pressure + fn pressure(&self) -> Pressure { + let r = ThermodynamicConstants::UniversalGasConstant.value().downcast::().unwrap(); + let ideal_pressure = (self.total_mol.get::() * r.get::() * self.temperature.get::()) / (self.total_vol.get::()); + Pressure::new::(ideal_pressure) + } + ///Deterrmine entropy + // Will need to use equation (5.10) from the 'Introduction to Chemical Engineering + // Thermodynamics' + fn entropy(&self) -> MolarHeatCapacity { + let mut entropy_total = 0.0; + let t_ref = 298.15_f64; //reference temperature + let mut cp_ref; + let mut cp_t; + let r = ThermodynamicConstants::UniversalGasConstant.value().downcast::().unwrap(); + let p_o = 1.0_f64; // units atm + + for chem_object in &self.species_list { + let chem = &(*chem_object).chemical_species.properties; + if chem.const_c != 0.0 { + cp_ref = chem.const_a * t_ref.ln() + (chem.const_b / (10.0f64.powf(3.0))) * t_ref; + cp_t = chem.const_a * self.temperature.get::().ln() + (chem.const_b / (10.0f64.powf(3.0))) * self.temperature.get::() + (1.0 / 2.0) * (chem.const_c / (10.0f64.powf(6.0))) * self.temperature.get::().powf(2.0); + } + else{ + cp_ref = chem.const_a * t_ref.ln() + (chem.const_b / (10.0f64.powf(3.0))) * t_ref + (-1.0/2.0) * (chem.const_d / (10.0f64.powf(-5.0))) * t_ref.powi(-2); + cp_t = chem.const_a * self.temperature.get::().ln() + (chem.const_b / (10.0f64.powf(3.0))) * self.temperature.get::() + (-1.0/2.0) * (chem.const_d / (10.0f64.powf(-5.0))) * self.temperature.get::().powf(-2.0); + } + let integral_solve_species = cp_t - cp_ref; + let pressure_ratio = (*chem_object).partial_pressure.get::() / p_o; + + entropy_total += (chem_object.molar_quantity.get::()/self.total_mol.get::())*r.get::()*(integral_solve_species - pressure_ratio); + } + + MolarHeatCapacity::new::(entropy_total) + } + /// Determining vapor fraction + // In Ideal gas package, only will be used when components are all in gaseous state so + // vapor fraction will always be equal to 1 + fn vapor_fraction(&self) -> Ratio { + Ratio::new::(1.0) + } + /// Determining Cp (Heat capacity under constant pressure conditions) + fn heat_capacity_const_pressure(&self) -> MolarHeatCapacity { + let r = ThermodynamicConstants::UniversalGasConstant.value().downcast::().unwrap(); + let mut total_heat_capacity_const_pressure : f64 = 0.0; + let mut cp_t; + let t = self.temperature.get::(); + for chem_object in &self.species_list { + let chem = &(*chem_object).chemical_species.properties; + if chem.const_c != 0.0 { + cp_t = chem.const_a + (chem.const_b / (10.0f64.powf(3.0)))*t + (chem.const_c / (10.0f64.powf(6.0)))*t.powf(2.0); + } + else { + cp_t = chem.const_a + (chem.const_b / (10.0f64.powf(3.0)))*t + (chem.const_d / (10.0f64.powf(-5.0)))*t.powf(-2.0); + } + total_heat_capacity_const_pressure += cp_t* (chem_object.molar_quantity.get::()/self.total_mol.get::())*r.get::(); + } + MolarHeatCapacity::new::(total_heat_capacity_const_pressure) + } + ///Determining internal energy + //Need to figure out way to calculate Cv + fn internal_energy(&self) -> MolarEnergy { + MolarEnergy::new::(0.0) + } + ///Determining temperature + fn temperature(&self) -> ThermodynamicTemperature { + let r = ThermodynamicConstants::UniversalGasConstant.value().downcast::().unwrap().get::(); + //T = PV/nR + let p = self.pressure.get::(); + let v = self.total_vol.get::(); + let n = self.total_mol.get::(); + let ideal_temperature = (p*v)/(n*r); + ThermodynamicTemperature::new::(ideal_temperature) + } + ///Determining volume + fn volume(&self) -> Volume { + let r = ThermodynamicConstants::UniversalGasConstant.value().downcast::().unwrap().get::(); + // V = (nRT)/P + let n = self.total_mol.get::(); + let p = self.pressure.get::(); + let t = self.temperature.get::(); + let ideal_volume = (n*r*t)/(p); + Volume::new::(ideal_volume) + } + ///Determining the Gibbs free energy + fn gibbs_free_energy(&self) -> Energy { + let enthalpy = self.enthalpy().get::()*self.total_mol.get::(); + let entropy = self.entropy().get::()*self.total_mol.get::(); + let gibbs_free_energy_value = enthalpy - self.temperature.get::()*entropy; + Energy::new::(gibbs_free_energy_value) + } +} + diff --git a/oscps-lib/src/thermodynamics/srk_package.rs b/oscps-lib/src/thermodynamics/srk_package.rs new file mode 100644 index 0000000..94a196a --- /dev/null +++ b/oscps-lib/src/thermodynamics/srk_package.rs @@ -0,0 +1,17 @@ + +///#SRKPackage +/// +///Will contain equations relating to the SRK Equation of state + + +use crate::thermodynamics::*; +use std::sync::Arc; +use uom::si::f64::*; +use uom::si::molar_energy; +use uom::si::molar_heat_capacity; +use uom::si::pressure; +use uom::si::thermodynamic_temperature; +use uom::si::energy; +use uom::si::amount_of_substance; +use uom::si::volume; +use uom::si::ratio;