angularsen / UnitsNet

Makes life working with units of measurement just a little bit better.
https://www.nuget.org/packages/UnitsNet/
MIT No Attribution
2.18k stars 377 forks source link

Standard and normal flow rates, SCFH and Nm^3/h #724

Closed sequc82 closed 4 years ago

sequc82 commented 4 years ago

First-time contributor here!

It looks like this feature idea has been addressed to some degree already in #212 and #713.

It is already clear that standard and normal flow rates are missing in UnitsNet. Units such as SCCM (standard cubic centimeters/min), SCFM (standard cubic feet/min), SCFH (standard cubic feet/hour), Nm3/h (normal cubic meters/hour), etc. are best described as the type MassFlowUnit rather than VolumeFlowUnit. This is due to the gas flow being constrained to Normal or Standard pressure and temperature conditions as previously mentioned in #212. As mentioned in #713, these units are typically used with mass flow meters and controllers.

Conversions from standard and normal flow rates to more conventional mass flow rates are then just constant conversions like any other with the exception that the specific gravity of the gas must also be accounted for.

For example, lb/h gas to SCFH gas: SCFH gas = lb/h*13.1/S.G. S.G.: Gas Specific Gravity (unitless)

Converting from mass flow in SCFH to volume flow in ACFH (actual cubic feet per hour) of course is more complex as has been discovered in #212 and #713.

How can I help?

Some references: Fisher Control Valve Handbook Chapter 15: Conversions and Equivalents Wikipedia: SCFH The Engineering Toolbox: Gas Specific Gravity

angularsen commented 4 years ago

Hi there, thanks for doing all the research on the existing issues and creating a summary here.

I don't know this domain at all and I currently don't what exactly is lacking in Units.NET or what needs to be added or changed. My time is also very limited these days with family and other projects, so if you could create a short bullet list of what concrete changes you'd like to see in Units.NET, or maybe you need some help from me to decide how to fit a new concept into Units.NET, then it's a lot easier for me to comment on it and help move this forward.

Should we add some new quantities? New units? Should some quantities be renamed/repurposed because they are wrong/ambiguous? Should we add some wrapper types that help with conversions by adding extra information that our current quantities don't provide?

Things like that. The more specific/concrete the better. Pseudocode examples of intended usage is also very nice.

I see I commented in #713 that my understanding was that maybe a wrapper type with some more context/information is needed to solve this.

Best, Andreas

sequc82 commented 4 years ago

I suppose it's also worth noting, this applies only to gas flow rather than liquid flow.

I'm still trying to familiarize myself with the architecture of UnitsNet. I assume every conversion across quantities requires a wrapper? I would expect that conversions to a StandardFlowUnit could be handled within StandardFlow, but would conversions from a StandardFlowUnit require a wrapper for both MassFlow and VolumeFlow?

So I suppose I'm proposing creating instances of StandardFlow in the forms below: from MassFlow

new StandardFlow(MassFlow massFlow, double gasSpecificGravity)
new StandardFlow(MassFlow massFlow, Density gasDensity)

or from VolumeFlow (for air)

new StandardFlow(VolumeFlow volumeFlow, Temperature fluidTemperature, ReferencePressure fluidPressure, ReferencePressure saturationPressure, double relativeHumidity, ReferencePressure standardPressure, Temperature standardTemperature)

We would see more varieties to the constructor from VolumeFlow rather than MassFlow to account for fluid density at different temperatures and pressures.

angularsen commented 4 years ago

I assume every conversion across quantities requires a wrapper?

It depends on what you mean. The architecture is currently designed to use a code generator to read JSON files like Length.json and output C# code with quantity types like Length and Mass. Quantities represent a value and unit and has properties and methods to convert between units of the same quantity.

We sometimes extend the generated code to allow for converting to other quantities, such as Mass m = Mass.FromGravitationalForce(myForce); or Area a = myLength1 * myLength2;.

Extensions like that are manually added to partial files like Mass.extra.cs and Length.extra.cs.

So if I understood you right, there is typically no wrapper type involved in cross-quantity conversions, only partial files that add extra methods and members to the generated quantity types.

So in your suggestion, if you are generating the StandardFlow type based off a JSON file like all the other quantities, then you could add a StandardFlow.extra.cs file with

public partial struct StandardFlow
{ 
    /* extra methods, properties etc here */ 
}

If you are NOT generating the code for StandardFlow, but rather implementing it by hand, then you can do whatever you want.

I'm slowly getting a better grip on this, but it would still help a lot for my understanding if you could show a couple of concrete, real-life examples of how you would like to use this in your day to day work:

sequc82 commented 4 years ago

@angularsen, Thank you for the clarification of the architecture.

Based on that, I suspect the best way to avoid code duplication would be to build upon VolumeFlow.

Because temperature, pressure, and fluid are held constant, a Standard, or Normal flow rate can be treated as MassFlow.

As I continue in detail on this topic, I start to wonder the same question of scope you mentioned previously in #713. Considering at the highest level though, VolumeFlow really just breaks down into compressible and incompressible flow, I can also see it being general enough to be within scope of the UnitsNet library.

angularsen commented 4 years ago

My head is exploding a bit on level of detail here :D It's great, just a lot to take in. We have some use cases now, but we still need to reduce this into some concrete changes to Units.NET.

would it be worth creating a layer of abstraction for something like IncompressibleVolumeFlow and CompressibleVolumeFlow to inherit VolumeFlow instead?

With struct quantity types we can unfortunately not inherit.

These sound to me like wrapper types that add extra information about the volume flows and you can add methods to the wrappers to convert between compressible and incompressible, which would change the internal VolumeFlow value accordingly.

Change proposal

Let's try to get a bit more concrete. I find that easier to discuss. Help me refine and formulate this.

Since a standard flow needs additional information beyond VolumeFlow such as pressure and temperature, we can't reuse code generator and need to write manual wrapper types.

Add wrapper types to represent StandardFlow and NormalFlow

Questions:

Convert between MassFlow|VolumeFlow and StandardFlow|NormalFlow

See code example below.

Parsing and ToString() of standard/normal flows

Do we need to support parsing strings like "1 SCF/HR" or "Sm³/hr"? If so, what are all the units and abbreviations we would like to parse?

Some examples here: https://www.traditionaloven.com/conversions_of_measures/gas_flows_converter_tool.html

Without code generator, we need to manually add support for parsing these.

As for ToString(), we could do it easy and add suffix "(standard)" or "(standard ISO)" behind the VolumeFlow.ToString(), to get something like 1 m³/h (standard ISO). It's better than nothing, but maybe not what you want.

If we add Parse() support, then it should be able to parse whatever ToString() outputs.

Example implementation for discussion

A single wrapper type to hold information for Standard/Normal flow rates, with methods to convert to/from MassFlow and VolumeFlow.

// Not sure about this name, trying find something that represents a flow at a certain condition, maybe FlowWithReferenceCondition or something
public abstract class ReferenceFlow
{
    public ReferenceFlow(MassFlow mf, Pressure p, Temperature t, ReferenceFlowCondition c) 
    { 
        /* assign properties here */ 
    }

    public VolumeFlow VolumeFlow { get; }
    public Pressure BasePressure { get; }
    public Temperature BaseTemperature { get; }

    // Arguably we don't need this if we use inheritance and separate types for each condition and can match on type instead, but I like having both
    public ReferenceFlowCondition ReferenceFlowCondition { get; }

    public MassFlow ToMassFlow() {}
    public VolumeFlow ToVolumeFlow() {}
}

public enum ReferenceFlowCondition
{
    StandardUS,
    StandardISO,
    StandardAGA,
    Normal,
    Custom
}

// Each standard as its own type, inheriting from base type
public class StandardFlowUS : ReferenceFlow
{
    public StandardFlowUS(/*args*/) : this(/*pass on args to ReferenceFlow ctor*/) {}    
}

public class StandardFlowISO : ReferenceFlow
{
    public StandardFlowISO(/*args*/) : this(/*pass on args to ReferenceFlow ctor*/) {}    
}

public class StandardFlowAGA : ReferenceFlow
{
    public StandardFlowAGA(/*args*/) : this(/*pass on args to ReferenceFlow ctor*/) {}    
}

public class NormalFlow : ReferenceFlow
{
    public NormalFlow(/*args*/) : this(/*pass on args to ReferenceFlow ctor*/) {}    
}

// The idea is to support custom conditions beyond the standard ones, I don't know if this will ever be useful though?
public class CustomReferenceFlow : ReferenceFlow
{
    public CustomReferenceFlow(/*args*/) : this(/*pass on args to ReferenceFlow ctor*/) {}    
}

// MassFlow.extra.cs
public static StandardFlowUS ToStandardFlowUS(this MassFlow mf, /* other args */) {}
public static StandardFlowISO ToStandardFlowISO(this MassFlow mf, /* other args */) {}
public static StandardFlowAGA ToStandardFlowAGA(this MassFlow mf, /* other args */) {}
public static NormalFlow ToNormalFlow(this MassFlow mf, /* other args */) {}

// VolumeFlow.extra.cs
public static StandardFlowUS ToStandardFlowUS(this VolumeFlow vf, /* other args */) {}
public static StandardFlowISO ToStandardFlowISO(this VolumeFlow vf, /* other args */) {}
public static StandardFlowAGA ToStandardFlowAGA(this VolumeFlow vf, /* other args */) {}
public static NormalFlow ToNormalFlow(this VolumeFlow vf, /* other args */) {}

Let me know what you think of this direction.

angularsen commented 4 years ago

Adding @bitbonk, @lipchev, @MarcDrexler to the discussion, since they have been involved in previous related issues.

sequc82 commented 4 years ago

This has been a long stream of consciousness...

I keep forgetting about the struct vs class debate.

I now think its best to describe standard/normal flows as VolumeFlow types, but can be converted to MassFlow types when accounting for fluid density (through Standard Temperature and Pressure).

While thinking through this some more, I realize we can simplify this to two separate problems:

Standard Temperature and Pressure StandardFlow and NormalFlow, along with US, ISO, AGA, etc., are still the same type, but with different values for Pressure, Temperature and Relative Humidity.

What if we created a EnvironmentalReference type, which stored the Pressure, Temperature and Relative Humidity for a given measurement? We could create a Dictionary of EnvironmentalReferences to store the values of known standard/normal conditions (US, ISO, AGA, etc.) to reference for use. At that, I suppose we could still rely on an enum value to index the Dictionary.

Note: I realized when thinking about STP, that the development of ReferencePressure currently assigns DefaultAtmosphericPressure as 1 atm. There is an overload to the ReferencePressure constructor to accept a different atmospheric Pressure, but it may be worth referencing an EnvironmentalReference.Pressure value instead.

A couple conversion thoughts: VolumeFlow Conversions:

MassFlow Conversions:

Parsing and ToString() You make a good point of supporting strings. While I haven't seen 1 SCF/HR before (typ. SCFH), I have seen Sm3/hr and Nm3/hr before.

As we can see above, with Nm3/hr, it could easily be mistaken for Newtons rather than Normal. Especially when you consider the number of standards to define EnvironmentalReference, I would think a prefix/suffix sort of idea could be acceptable to maintain clarity. Perhaps in cases where the flow condition is not at a particular standard, the same suffix could instead specify the flow conditions in detail to get something like 1 m3/h (at 300 K, and 1 atm).

I would suggest additionally including the fluid in the suffix when deviating from the standard specification, such as 1 m3/h (dry nitrogen, standard ISO), or to the same degree, 1 m3/h (air, at 300 K, 1 atm, and 20% R.H.).

For example

public enum StandardReference
{
    StandardUS,
    StandardISO,
    StandardAGA,
    Normal,
    Custom
}

public struct EnvironmentalReference
{
    public EnvironmentalReference(Pressure p, Temperature t, double RelativeHumidity)
    {
        /*Assign properties here*/
    }

    public static Dictionary<StandardReference, EnvironmentalReference> StandardReferences { get; } = new Dictionary<StandardReference, EnvironmentalReference>()
        {
            {StandardReference.StandardUS, new EnvironmentalReference(Pressure.FromPoundsForcePerSquareInch(14.696), Temperature.FromDegreesFahrenheit(60), 0)},
            /*Define subsequent StandardReferences here*/
        };
{

public struct ReferenceVolumeFlow
{
    public ReferenceVolumeFlow(VolumeFlow q, EnvironmentalReference e,  MolarMass m)
    {
        /*Assign properties here*/
        /*Calculate density here*/
    }
    public ReferenceVolumeFlow(VolumeFlow q, Pressure p, Temperature t, MolarMass m)
    {
        /*Assign properties here*/
        /*Calculate density here*/
    }
    public ReferenceVolumeFlow(VolumeFlow q, Pressure p, Temperature t, MolarMass m, CompressibilityFactor z)
    {
        /*Assign properties here*/
        /*Calculate density here*/
    }
    public ReferenceVolumeFlow(VolumeFlow q, Density d)
    {
        /*Assign density here*/
    }
    public ReferenceVolumeFlow ToCondition (Pressure p, Temperature t)
    {
        /*pass on args to ReferenceVolumeFlow ctor*/
    }
    public ReferenceVolumeFlow ToCondition (EnvironmentalReference e)
    {
        /*pass on args to ReferenceVolumeFlow ctor*/
    }
    public ReferenceVolumeFlow ToFluid (MolarMass m, CompressibilityFactor z)
    {
        /*pass on args to ReferenceVolumeFlow ctor*/
    }
    public MassFlow ToMassFlow (MassFlowUnit u)
    {
        /*Calculate from properties*/
        /*Pass on args to MassFlow ctor*/
    }
    /*Other overloads*/
{

//MassFlow.Extra.cs
public static ReferenceVolumeFlow ToReferenceVolumeFlow(this MassFlow mf, /* other args */) {}
//VolumeFlow.Extra.cs
public static ReferenceVolumeFlow ToReferenceVolumeFlow(this VolumeFlow vf, /* other args */) {}
angularsen commented 4 years ago

Can we wrap VolumeFlow in a way that allows us to reuse the existing code generator conversions?

Not easy as of today, to my knowledge. I don't want to resort to manually keeping any hard coded delegate methods in the wrapper type in sync whenever conversion methods change in MassFlow or VolumeFlow. We could maybe extend the code generator to support this use case.

To convert VolumeFlow to MassFlow, gas flow requires some information about the physical properties of the gas flowed. Should we include direct conversions of VolumeFlow between gases?

This would be trivial to add with some extension methods that take the relevant arguments to convert between those two, as detailed earlier. To distinguish between ideal and real gases, I'd suggest including that in the name of the conversion methods, maybe like this ToMassFlowForIdealGas(this VolumeFlow idealGasFlow, /*other args*/).

As we can see above, with Nm3/hr, it could easily be mistaken for Newtons rather than Normal.

Yes, but parsing is mutually exclusive to each quantity type, so for VolumeFlow.Parse("1 Nm3/hr") it should work as long as this abbreviation is unique and unambiguous for all volume flow units. It will not conflict with Force.Parse() and other quantities.

specially when you consider the number of standards to define EnvironmentalReference, I would think a prefix/suffix sort of idea could be acceptable to maintain clarity.

Good, that is easier to do.

Perhaps in cases where the flow condition is not at a particular standard, the same suffix could instead specify the flow conditions in detail to get something like 1 m3/h (at 300 K, and 1 atm).

Good suggestion, I like it.

I would suggest additionally including the fluid in the suffix when deviating from the standard specification, such as 1 m3/h (dry nitrogen, standard ISO), or to the same degree, 1 m3/h (air, at 300 K, 1 atm, and 20% R.H.).

OK, if that makes sense to your domain and usage, then sure. It would require specifying this when constructing the wrapper quantity type.

Great example, some comments:

bitbonk commented 4 years ago

Just as a side note: While it is nice to have conversions built in between the different flow quantities, for us it would be enough just to have the standard flow quantity (including the unit sccm, displayable as "sccm"). We will hardly ever have the need for such conversions.

I suspect that if the conversions between the different flow quantities is not built in, it would still be possible to do it manually, right?

sequc82 commented 4 years ago

@bitbonk, Does that mean the only functionality you need is around parsing and ToString(), including shorthand strings like SCCM? The only problem I foresee with shorthand strings like SCCM, SCFM, SCFH, etc. is the ambiguity of STP for example, SCFM wiki. I haven't yet figured out a good way to avoid the ambiguity.

@angularsen

To distinguish between ideal and real gases, I'd suggest including that in the name of the conversion methods, maybe like this ToMassFlowForIdealGas(this VolumeFlow idealGasFlow, /*other args*/).

  • ReferenceVolumeFlow does not currently
    • hold any information about whether it represents an ideal gas flow, real gas flow or liquid flow. Should that be included?

I like the suggestion for distinguishing between ideal and real conversion methods. I'm not sure if we would need to store information on ideal, real or liquid, since the relevant conversions would be to or from MassFlow, and would be identified by the conversion method. What do you think?

  • hold information about the fluid, like dry nitrogen

That is a good point. I referenced it when discussing string parsing, and jumped to its properties, like MolarMass or CompressibilityFactor, but didn't connect the two. Would it be worth creating a struct, enum, and Dictionary like EnvironmentalReference, where common fluids are stored in the Dictionary, but the struct allows for creating unique fluids?

More nitpicky on my end, but it slipped my mind that compressibility factor is actually a calculated value from gas Density (at a given Temperature and ReferencePressure) and MolarMass, so the stored physical properties would not need to include compressibility factor.

For example

public enum CommonFluid
{
    DryNitrogen,
    NaturalGas,
    LiquidPropane,
    DryAir,
    Water
}

public struct Fluid
{
    public Fluid(string Name, MolarMass m, Density d, ReferencePressure p, Temperature t, /*Any other relevant physical properties*/ )
    {
        /*Assign properties here*/
    }

    public static Dictionary<CommonFluid, Fluid> CommonFluid{ get; } = new Dictionary<CommonFluid, Fluid>()
        {
            {CommonFluid.DryNitrogen, new Fluid("Dry Nitrogen, MolarMass.FromGramPerMole(28.013), Density.FromGramPerLiter(1.126), new ReferencePressure(Pressure.FromPoundsForcePerSquareInch(14.696)), Temperature.FromDegreesFahrenheit(60))} ,
            /*Define subsequent CommonFluids here*/
        };
{
  • hold information about StandardReference enum it was constructed with, neither as part of EnvironmentalReference where maybe it belongs

I agree this one needs to be addressed. I would think EnvironmentalReference would be similar to UnitSystem, with the STP conditions being similar to BaseUnits, and the Dictionary of StandardReference would be similar to UnitSystem.SI if it stored a collection of UnitSystem instead of only SI.

I think storage at that level deals with the issue I recognized in my last comment about ReferencePressure as well.

Note: I realized when thinking about STP, that the development of ReferencePressure currently assigns DefaultAtmosphericPressure as 1 atm. There is an overload to the ReferencePressure constructor to accept a different atmospheric Pressure, but it may be worth referencing an EnvironmentalReference.Pressure value instead.

Would you be able to clarify the storage of those objects, and share your thoughts on this idea?

lipchev commented 4 years ago

I think I've mentioned this before, but I still wonder if the notion of a "reference scale" is not something that's fundamentally missing from our quantities. I guess you could put all of the conversion code in the *.extra.cs if you had access to the reference scale as part of the quantity. I mean we already have some implicit one dimensional scale: f(x)->x for [double.MinValue. double.MaxValue]. Give it some type/struct, a decent name and put as the default value for the construction of normal quantities- all arithmetic operations operating on the same scale are preserved- operations on different scales are either not supported or use some special mapping function from one scale to another. Custom scale classes are created per quantity, are strongly typed and hold whatever necessary information. We add a property to the json schema that associates the quantity with the special type- and the generator uses it to create a field of the class type. So, what other quantities could benefit from such an addition? Temperature, Pressure, Density? Also note the reduction of dimensions for the VolumeFlowReferenceScale with the possible addition of a DensityReferenceScale (to the Density unit).

lipchev commented 4 years ago

Hmm, here's one final thought(possibly out of scope): you know how we loose precision with units that are far away from the base unit- what if some basic quantity, like Length had multiple possible scales- e.g. Macro, Normal, Micro- and the scales corresponded to some mapping function like f(x)->10e6 * x that is applied on the conversion coefficient- then adding for instance two very small lengths with different units, but in the same scale should preserve some of the precision of the result.

bitbonk commented 4 years ago

@sequc82

Does that mean the only functionality you need is around parsing and ToString(), including shorthand strings like SCCM?

We would like to have the standard flow quantity that has parsing and ToString() for SCCM and that can convert between different units within that quantity (eg. from SCCM to SCM). Conversion to other quantitites is nice to have but not required. So I would say the answer to your question is yes.

angularsen commented 4 years ago

This is great! 😀 People who know the domain and will be using this stuff needs to move this design forward. I can assist in how to best fit it into UnitsNet code base and its overall architecture, but you guys need to figure out how it best solves your needs.

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

rohahn commented 3 years ago

I created a PR for the simple version without complex conversions proposed by @bitbonk.