#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2023 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
r"""
Crusher
=======
Author: Lingyan Deng
The Crusher module includes power consumption for solid crushing. It is a function of particle size distribution, mass flow rate, and bond work index.
Degrees of Freedom
------------------
A Crusher module has two degrees of freedom, which are the output of "particle_size_median" and "particle_size_width".
Model Structure
---------------
The Crusher model includes one inlet Port (inlet) and one outlet Port (outlet). The properties of the Crusher Unit model is mainly the particle size distribution.
Additional Constraints
----------------------
Crusher adds one additional constraint to calculate the work required to crush the particles.
.. math:: work_{t} = 10 * m_{t, in} * BWI * \left(\frac{1}{\sqrt{P_{t, prod, 80}}} - \frac{1}{\sqrt{P_{t, feed, 80}}}\right)
where :math:`work_{t}` is the work required to crush the particles at :math:`t` time, 10 is an empirical value and should not be changed, :math:`m_{t, in}` is the inlet mass flow rate
at :math:`t` time, :math:`BWI` is the bond work index of particles, :math:`P_{t, prod, 80}` is production particle size with 80% passing the mesh at :math:`t` time, :math:`P_{t, feed, 80}` is
feed particle size with 80% passing the mesh at :math:`t` time.
Expressions
-----------
Crusher includes two expressions to calculate the size of particles that has 80% passing the mesh for both feed and product particles.
.. math:: P_{t, feed, 80} = \frac{S_{t, in, median}}{unit} * \left(-\log(1 - 0.8)\right)^{\frac{SW_{t, in}}{2}}
where :math:`P_{t, feed, 80}` is the feed particle size that has 80% passing the mesh at :math:`t` time, :math:`\frac{S_{t, in, median}}{unit}` is the median particle size of input at :math:`t` time and unitless. The
default particle size is in micrometer. The :math:`SW_{t, in}` is the particle size width of input at :math:`t` time.
.. math:: P_{t, prod, 80} = \frac{S_{t, out, median}}{unit} * \left(-\log(1 - 0.8)\right)^{\frac{SW_{t, out}}{2}}
where :math:`P_{t, prod, 80}` is the product particle size that has 80% passing the mesh at :math:`t` time, :math:`\frac{S_{t, out, median}}{unit}` is the median particle size of output at :math:`t` time and unitless. The
default particle size is in micrometer. The :math:`SW_{t, in}` is the particle size width of output at :math:`t` time.
Variables
---------
Crusher add the following additional variables beyond those created in property packages.
================ ====== ================================================================================================
Variable Name Notes
================ ====== ================================================================================================
:math:`work_{t}` work
================ ====== ================================================================================================
"""
from functools import partial
from pyomo.environ import Var, log, Constraint, units as pyunits
from pyomo.common.config import ConfigValue, ConfigDict, In
from idaes.core import (
declare_process_block_class,
UnitModelBlockData,
useDefault,
)
from idaes.core.util.config import is_physical_parameter_block
from idaes.core.util.tables import create_stream_table_dataframe
import idaes.logger as idaeslog
_log = idaeslog.getLogger(__name__)
[docs]
@declare_process_block_class("Crusher")
class CrusherData(UnitModelBlockData):
CONFIG = ConfigDict()
CONFIG.declare(
"dynamic",
ConfigValue(
domain=In([False]),
default=False,
description="Dynamic model flag - must be False",
doc="""Crush unit is steady-state only""",
),
)
CONFIG.declare(
"has_holdup",
ConfigValue(
default=False,
domain=In([False]),
description="Holdup construction flag - must be False",
doc="""Crush unit has no holdup.""",
),
)
CONFIG.declare(
"property_package",
ConfigValue(
default=useDefault,
domain=is_physical_parameter_block,
description="Property package to use for control volume",
doc="""Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
),
)
CONFIG.declare(
"property_package_args",
ConfigDict(
implicit=True,
description="Arguments to use for constructing property packages",
doc="""A ConfigDict with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
),
)
[docs]
def build(self):
"""
Begin building model (pre-DAE transformation).
Args:
None
Returns:
None
"""
# Call UnitModel.build to setup dynamics
super().build()
# Build state blocks.
self.properties_in = self.config.property_package.build_state_block(
self.flowsheet().time,
defined_state=True,
**self.config.property_package_args,
)
self.properties_out = self.config.property_package.build_state_block(
self.flowsheet().time,
defined_state=True,
**self.config.property_package_args,
)
tref = self.flowsheet().time.first()
statevars = self.properties_in[tref].define_state_vars()
# A constraint of In=Out for all state variables which are not related to particle size.
for k, v in statevars.items():
if k not in ["particle_size_median", "particle_size_width"]:
idx = v.index_set()
c = Constraint(
self.flowsheet().time,
idx,
doc=f"{k} constraint",
rule=partial(_state_rule, state=k),
)
self.add_component(k + "_constraint", c)
# Add Ports
self.add_port("inlet", self.properties_in)
self.add_port("outlet", self.properties_out)
self.work = Var(
self.flowsheet().time,
units=pyunits.W,
initialize=3915.17,
doc="Work required to increase crush the solid",
)
""" Breakage Distribution calculation as a constraint.
This is the equation for accumulative fraction of solid breakage
probability distribution smaller than size x=feed_p80
"""
sunit = self.properties_in[tref].particle_size_median.get_units()
@self.Expression(self.flowsheet().time, doc="Feed P80 size")
def feed_p80(self, t):
return (
self.properties_in[t].particle_size_median
/ sunit
* (-log(1 - 0.8)) ** (self.properties_in[t].particle_size_width / 2)
)
@self.Expression(self.flowsheet().time, doc="Product P80 size")
def prod_p80(self, t):
return (
self.properties_out[t].particle_size_median
/ sunit
* (-log(1 - 0.8)) ** (self.properties_out[t].particle_size_width / 2)
)
@self.Constraint(self.flowsheet().time, doc="Crusher work constraint")
def crush_work_eq(self, t):
return self.work[t] == (
10 # 10 is an empirical correlation, this should not be changed.
* self.properties_in[t].flow_mass
* self.config.property_package.bond_work_index
* (1 / (self.prod_p80[t]) ** 0.5 - 1 / (self.feed_p80[t]) ** 0.5)
)
def _get_stream_table_contents(self, time_point=0):
# Dictionary to hold data for all streams
io_dict = {"Inlet": self.properties_in, "Outlet": self.properties_out}
return create_stream_table_dataframe(io_dict, time_point=time_point)
def _get_performance_contents(self, time_point=0):
# Report
var_dict = {
"Work Required (W)": self.work[time_point].value,
}
return {"vars": var_dict}
def _state_rule(b, time, index, state):
sin = b.properties_in[time].define_state_vars()[state]
sout = b.properties_out[time].define_state_vars()[state]
return sin[index] == sout[index]