1   /* Copyright 2002-2024 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.utils.units;
18  
19  import java.io.Serializable;
20  import java.util.List;
21  
22  import org.hipparchus.CalculusFieldElement;
23  import org.hipparchus.fraction.Fraction;
24  import org.hipparchus.util.FastMath;
25  import org.hipparchus.util.Precision;
26  import org.orekit.errors.OrekitException;
27  import org.orekit.errors.OrekitMessages;
28  
29  /** Basic handling of multiplicative units.
30   * <p>
31   * This class is by no means a complete handling of units. For complete
32   * support, look at libraries like {@code UOM}. This class handles only
33   * time, length, mass and current dimensions, as well as angles (which are
34   * dimensionless).
35   * </p>
36   * <p>
37   * Instances of this class are immutable.
38   * </p>
39   * @see <a href="https://github.com/netomi/uom">UOM</a>
40   * @author Luc Maisonobe
41   * @since 11.0
42   */
43  public class Unit implements Serializable {
44  
45      /** No unit. */
46      public static final Unit NONE = new Unit("n/a", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
47  
48      /** Dimensionless unit. */
49      public static final Unit ONE = new Unit("1", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
50  
51      /** Cycle unit.
52       * @since 13.0
53       */
54      public static final Unit CYCLE = new Unit("cyc", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
55  
56      /** Percentage unit. */
57      public static final Unit PERCENT = new Unit("%", 1.0e-2, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
58  
59      /** Second unit. */
60      public static final Unit SECOND = new Unit("s", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ONE, Fraction.ZERO, Fraction.ZERO);
61  
62      /** Minute unit. */
63      public static final Unit MINUTE = SECOND.scale("min", 60.0);
64  
65      /** Hour unit. */
66      public static final Unit HOUR = MINUTE.scale("h", 60);
67  
68      /** Day unit. */
69      public static final Unit DAY = HOUR.scale("d", 24.0);
70  
71      /** Julian year unit.
72       * @see <a href="https://www.iau.org/publications/proceedings_rules/units/">SI Units at IAU</a>
73       */
74      public static final Unit YEAR = DAY.scale("a", 365.25);
75  
76      /** Hertz unit. */
77      public static final Unit HERTZ = SECOND.power("Hz", Fraction.MINUS_ONE);
78  
79      /** Metre unit. */
80      public static final Unit METRE = new Unit("m", 1.0, Fraction.ZERO, Fraction.ONE, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
81  
82      /** Kilometre unit. */
83      public static final Unit KILOMETRE = METRE.scale("km", 1000.0);
84  
85      /** Kilogram unit. */
86      public static final Unit KILOGRAM = new Unit("kg", 1.0, Fraction.ONE, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
87  
88      /** Gram unit. */
89      public static final Unit GRAM = KILOGRAM.scale("g", 1.0e-3);
90  
91      /** Ampere unit. */
92      public static final Unit AMPERE = new Unit("A", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ONE, Fraction.ZERO);
93  
94      /** Radian unit. */
95      public static final Unit RADIAN = new Unit("rad", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ONE);
96  
97      /** Degree unit. */
98      public static final Unit DEGREE = RADIAN.scale("°", FastMath.toRadians(1.0));
99  
100     /** Arc minute unit. */
101     public static final Unit ARC_MINUTE = DEGREE.scale("′", 1.0 / 60.0);
102 
103     /** Arc second unit. */
104     public static final Unit ARC_SECOND = ARC_MINUTE.scale("″", 1.0 / 60.0);
105 
106     /** Revolution unit. */
107     public static final Unit REVOLUTION = RADIAN.scale("rev", 2.0 * FastMath.PI);
108 
109     /** Newton unit. */
110     public static final Unit NEWTON = KILOGRAM.multiply(null, METRE).divide("N", SECOND.power(null, Fraction.TWO));
111 
112     /** Pascal unit. */
113     public static final Unit PASCAL = NEWTON.divide("Pa", METRE.power(null, Fraction.TWO));
114 
115     /** Bar unit. */
116     public static final Unit BAR = PASCAL.scale("bar", 100000.0);
117 
118     /** Joule unit. */
119     public static final Unit JOULE = NEWTON.multiply("J", METRE);
120 
121     /** Watt unit. */
122     public static final Unit WATT = JOULE.divide("W", SECOND);
123 
124     /** Coulomb unit. */
125     public static final Unit COULOMB = SECOND.multiply("C", AMPERE);
126 
127     /** Volt unit. */
128     public static final Unit VOLT = WATT.divide("V", AMPERE);
129 
130     /** Ohm unit. */
131     public static final Unit OHM = VOLT.divide("Ω", AMPERE);
132 
133     /** tesla unit. */
134     public static final Unit TESLA = VOLT.multiply(null, SECOND).divide("T", METRE.power(null, Fraction.TWO));
135 
136     /** Solar Flux Unit. */
137     public static final Unit SOLAR_FLUX_UNIT = WATT.divide(null, METRE.power(null, Fraction.TWO).multiply(null, HERTZ)).scale("SFU", 1.0e-22);
138 
139     /** Total Electron Content Unit. */
140     public static final Unit TOTAL_ELECTRON_CONTENT_UNIT = METRE.power(null, new Fraction(-2)).scale("TECU", 1.0e+16);
141 
142     /** Earth Radii used as Bstar unit in CCSDS OMM. */
143     public static final Unit EARTH_RADII = new Unit("ER", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ONE, Fraction.ZERO);
144 
145     /** Serializable UID. */
146     private static final long serialVersionUID = 20210402L;
147 
148     /** Name name of the unit. */
149     private final String name;
150 
151     /** Scaling factor to SI units. */
152     private final double scale;
153 
154     /** Mass exponent. */
155     private final Fraction mass;
156 
157     /** Length exponent. */
158     private final Fraction length;
159 
160     /** Time exponent. */
161     private final Fraction time;
162 
163     /** Current exponent. */
164     private final Fraction current;
165 
166     /** Angle exponent. */
167     private final Fraction angle;
168 
169     /** Simple constructor.
170      * @param name name of the unit
171      * @param scale scaling factor to SI units
172      * @param mass mass exponent
173      * @param length length exponent
174      * @param time time exponent
175      * @param current current exponent
176      * @param angle angle exponent
177      */
178     public Unit(final String name, final double scale,
179                 final Fraction mass, final Fraction length,
180                 final Fraction time, final Fraction current,
181                 final Fraction angle) {
182         this.name    = name;
183         this.scale   = scale;
184         this.mass    = mass;
185         this.length  = length;
186         this.time    = time;
187         this.current = current;
188         this.angle   = angle;
189     }
190 
191     /** Get the name of the unit.
192      * @return name of the unit
193      */
194     public String getName() {
195         return name;
196     }
197 
198     /** Get the scaling factor to SI units.
199      * @return scaling factor to SI units
200      */
201     public double getScale() {
202         return scale;
203     }
204 
205     /** Get the mass exponent.
206      * @return mass exponent
207      */
208     public Fraction getMass() {
209         return mass;
210     }
211 
212     /** Get the length exponent.
213      * @return length exponent
214      */
215     public Fraction getLength() {
216         return length;
217     }
218 
219     /** Get the time exponent.
220      * @return time exponent
221      */
222     public Fraction getTime() {
223         return time;
224     }
225 
226     /** Get the current exponent.
227      * @return current exponent
228      */
229     public Fraction getCurrent() {
230         return current;
231     }
232 
233     /** Get the angle exponent.
234      * @return angle exponent
235      */
236     public Fraction getAngle() {
237         return angle;
238     }
239 
240     /** Check if a unit has the same dimension as another unit.
241      * @param other other unit to check against
242      * @return true if unit has the same dimension as the other unit
243      */
244     public boolean sameDimension(final Unit other) {
245         return time.equals(other.time) && length.equals(other.length)   &&
246                mass.equals(other.mass) && current.equals(other.current) &&
247                angle.equals(other.angle);
248     }
249 
250     /** Create the SI unit with same dimension.
251      * @return a new unit, with same dimension as instance and scaling factor set to 1.0
252      */
253     public Unit sameDimensionSI() {
254         final StringBuilder builder = new StringBuilder();
255         append(builder, KILOGRAM.name, mass);
256         append(builder, METRE.name,    length);
257         append(builder, SECOND.name,   time);
258         append(builder, AMPERE.name,   current);
259         append(builder, RADIAN.name,   angle);
260         if (builder.length() == 0) {
261             builder.append('1');
262         }
263         return new Unit(builder.toString(), 1.0, mass, length, time, current, angle);
264     }
265 
266     /** Ensure some units are compatible with reference units.
267      * @param description description of the units list (for error message generation)
268      * @param reference reference units
269      * @param units units to check
270      * @param allowScaleDifferences if true, unit with same dimension but different
271      * scale (like {@link #KILOMETRE} versus {@link #METRE}) are allowed, otherwise they will trigger an exception
272      * @exception OrekitException if units are not compatible (number of elements, dimensions or scaling)
273      */
274     public static void ensureCompatible(final String description, final List<Unit> reference,
275                                         final boolean allowScaleDifferences, final List<Unit> units) {
276         if (units.size() != reference.size()) {
277             throw new OrekitException(OrekitMessages.WRONG_NB_COMPONENTS,
278                                       description, reference.size(), units.size());
279         }
280         for (int i = 0; i < reference.size(); ++i) {
281             if (!reference.get(i).sameDimension(units.get(i))) {
282                 throw new OrekitException(OrekitMessages.INCOMPATIBLE_UNITS,
283                                           reference.get(i).getName(),
284                                           units.get(i).getName());
285             }
286             if (!(allowScaleDifferences ||
287                   Precision.equals(reference.get(i).getScale(), units.get(i).getScale(), 1))) {
288                 throw new OrekitException(OrekitMessages.INCOMPATIBLE_UNITS,
289                                           reference.get(i).getName(),
290                                           units.get(i).getName());
291             }
292         }
293     }
294 
295     /** Append a dimension contribution to a unit name.
296      * @param builder builder for unit name
297      * @param dim name of the dimension
298      * @param exp exponent of the dimension
299      */
300     private void append(final StringBuilder builder, final String dim, final Fraction exp) {
301         if (!exp.isZero()) {
302             if (builder.length() > 0) {
303                 builder.append('.');
304             }
305             builder.append(dim);
306             if (exp.getDenominator() == 1) {
307                 if (exp.getNumerator() != 1) {
308                     builder.append(Integer.toString(exp.getNumerator()).
309                                    replace('-', '⁻').
310                                    replace('0', '⁰').
311                                    replace('1', '¹').
312                                    replace('2', '²').
313                                    replace('3', '³').
314                                    replace('4', '⁴').
315                                    replace('5', '⁵').
316                                    replace('6', '⁶').
317                                    replace('7', '⁷').
318                                    replace('8', '⁸').
319                                    replace('9', '⁹'));
320                 }
321             } else {
322                 builder.
323                     append("^(").
324                     append(exp.getNumerator()).
325                     append('/').
326                     append(exp.getDenominator()).
327                     append(')');
328             }
329         }
330     }
331 
332     /** Create an alias for a unit.
333      * @param newName name of the new unit
334      * @return a new unit representing same unit as the instance but with a different name
335      */
336     public Unit alias(final String newName) {
337         return new Unit(newName, scale, mass, length, time, current, angle);
338     }
339 
340     /** Scale a unit.
341      * @param newName name of the new unit
342      * @param factor scaling factor
343      * @return a new unit representing scale times the instance
344      */
345     public Unit scale(final String newName, final double factor) {
346         return new Unit(newName, factor * scale, mass, length, time, current, angle);
347     }
348 
349     /** Create power of unit.
350      * @param newName name of the new unit
351      * @param exponent exponent to apply
352      * @return a new unit representing the power of the instance
353      */
354     public Unit power(final String newName, final Fraction exponent) {
355 
356         final int num = exponent.getNumerator();
357         final int den = exponent.getDenominator();
358         double s = (num == 1) ? scale : FastMath.pow(scale, num);
359         if (den > 1) {
360             if (den == 2) {
361                 s = FastMath.sqrt(s);
362             } else if (den == 3) {
363                 s = FastMath.cbrt(s);
364             } else {
365                 s = FastMath.pow(s, 1.0 / den);
366             }
367         }
368 
369         return new Unit(newName, s,
370                         mass.multiply(exponent), length.multiply(exponent),
371                         time.multiply(exponent), current.multiply(current),
372                         angle.multiply(exponent));
373     }
374 
375     /** Create root of unit.
376      * @param newName name of the new unit
377      * @return a new unit representing the square root of the instance
378      */
379     public Unit sqrt(final String newName) {
380         return new Unit(newName, FastMath.sqrt(scale),
381                         mass.divide(2), length.divide(2),
382                         time.divide(2), current.divide(2),
383                         angle.divide(2));
384     }
385 
386     /** Create product of units.
387      * @param newName name of the new unit
388      * @param other unit to multiply with
389      * @return a new unit representing the this times the other unit
390      */
391     public Unit multiply(final String newName, final Unit other) {
392         return new Unit(newName, scale * other.scale,
393                         mass.add(other.mass), length.add(other.length),
394                         time.add(other.time), current.add(other.current),
395                         angle.add(other.angle));
396     }
397 
398     /** Create quotient of units.
399      * @param newName name of the new unit
400      * @param other unit to divide with
401      * @return a new unit representing the this divided by the other unit
402      */
403     public Unit divide(final String newName, final Unit other) {
404         return new Unit(newName, scale / other.scale,
405                         mass.subtract(other.mass), length.subtract(other.length),
406                         time.subtract(other.time), current.subtract(other.current),
407                         angle.subtract(other.angle));
408     }
409 
410     /** Convert a value to SI units.
411      * @param value value instance unit
412      * @return value in SI units
413      */
414     public double toSI(final double value) {
415         return value * scale;
416     }
417 
418     /** Convert a value to SI units.
419      * @param value value instance unit
420      * @return value in SI units
421      */
422     public double toSI(final Double value) {
423         return value == null ? Double.NaN : value.doubleValue() * scale;
424     }
425 
426     /** Convert a value to SI units.
427      * @param <T> type of the field elements
428      * @param value value instance unit
429      * @return value in SI units
430      * @since 12.1
431      */
432     public <T extends CalculusFieldElement<T>> T toSI(final T value) {
433         return value.multiply(scale);
434     }
435 
436     /** Convert a value from SI units.
437      * @param value value SI unit
438      * @return value in instance units
439      */
440     public double fromSI(final double value) {
441         return value / scale;
442     }
443 
444     /** Convert a value from SI units.
445      * @param value value SI unit
446      * @return value in instance units
447      */
448     public double fromSI(final Double value) {
449         return value == null ? Double.NaN : value.doubleValue() / scale;
450     }
451 
452     /** Convert a value from SI units.
453      * @param <T> type of the field elements
454      * @param value value SI unit
455      * @return value in instance units
456      */
457     public <T extends CalculusFieldElement<T>> T fromSI(final T value) {
458         return value.divide(scale);
459     }
460 
461     /** Parse a unit.
462      * <p>
463      * The grammar for unit specification allows chains units multiplication and
464      * division, as well as putting powers on units.
465      * </p>
466      * <p>The symbols used for units are the SI units with some extensions.
467      * </p>
468      * <dl>
469      *   <dt>year</dt>
470      *   <dd>the accepted non-SI unit for Julian year is "a" but we also accept "yr"</dd>
471      *   <dt>day</dt>
472      *   <dd>the accepted non-SI unit for day is "d" but we also accept "day"</dd>
473      *   <dt>dimensionless</dt>
474      *   <dd>both "1" and "#" (U+0023, NUMBER SIGN) are accepted</dd>
475      *   <dt>mass</dt>
476      *   <dd>"g" is the standard symbol, despite the unit is "kg" (it is the only
477      *       unit that has a prefix in its name, so all multiples must be based on "g")</dd>
478      *   <dt>degrees</dt>
479      *   <dd>the base symbol for degrees is "°" (U+00B0, DEGREE SIGN), but we also accept
480      *       "◦" (U+25E6, WHITE BULLET) and "deg"</dd>
481      *   <dt>arcminute</dt>
482      *   <dd>The base symbol for arcminute is "′" (U+2032, PRIME) but we also accept "'" (U+0027, APOSTROPHE)</dd>
483      *   <dt>arcsecond</dt>
484      *   <dd>The base symbol for arcsecond is "″" (U+2033, DOUBLE PRIME) but we also accept
485      *   "''" (two occurrences of U+0027, APOSTROPHE), "\"" (U+0022, QUOTATION MARK) and "as"</dd>
486      * </dl>
487      * <p>
488      * All the SI prefix (from "y", yocto, to "Y", Yotta) are accepted, as well
489      * as integer prefixes. The standard symbol for micro 10⁻⁶ is "µ" (U+00B5, MICRO SIGN),
490      * but we also accept "μ" (U+03BC, GREEK SMALL LETTER MU). Beware that some combinations
491      * are forbidden, for example "Pa" is Pascal, not peta-years, and "as" is arcsecond for
492      * this parser, not atto-seconds, because many people in the space field use mas for
493      * milliarcseconds and µas for microarcseconds. Beware that prefixes are case-sensitive!
494      * Integer prefixes can be used to specify units like "30s", but only once at the beginning
495      * of the specification (i.e. "2rev/d²" is accepted, but "rev/(2d)²" is refused). Conforming
496      * with SI brochure "The International System of Units" (9th edition, 2019), each SI
497      * prefix is part of the unit and precedes the unit symbol without a separator
498      * (i.e. MHz is seen as one identifier).
499      * </p>
500      * <dl>
501      *   <dt>multiplication</dt>
502      *   <dd>can specified with either "*" (U+002A, ASTERISK), "×" (U+00D7, MULTIPLICATION SIGN),
503      *   "." (U+002E, FULL STOP) or "·" (U+00B7, MIDDLE DOT) as the operator</dd>
504      *   <dt>division</dt>
505      *   <dd>can be specified with either "/" (U+002F, SOLIDUS) or "⁄" (U+2044, FRACTION SLASH)
506      *   as the operator</dd>
507      *   <dt>powers</dt>
508      *   <dd>can be specified either by
509      *     <ul>
510      *       <li>prefixing with the unicode "√" (U+221A, SQUARE ROOT) character</li>
511      *       <li>postfixing with "**", "^" or implicitly using unicode superscripts</li>
512      *     </ul>
513      *   </dd>
514      * </dl>
515      * <p>
516      * Exponents can be specified in different ways:
517      * <ul>
518      *   <li>as an integer, as in "m^-2" or "m⁻²"</li>
519      *   <li>directly as unicode characters for the few fractions that unicode supports, as in "Ω^⅞"</li>
520      *   <li>as the special decimal value 0.5 which is used by CCSDS, as in "km**0.5"</li>
521      *   <li>as a pair of parentheses surrounding two integers separated by a solidus or fraction slash,
522      *   as in "Pa^(11/12)"</li>
523      * </ul>
524      * For integer exponents, the digits must be ASCII digits from the Basic Latin block from
525      * unicode if explicit exponent marker "**" or "^" is used, or using unicode superscript
526      * digits if implicit exponentiation (i.e. no markers at all) is used. Unicode superscripts
527      * are not allowed for fractional exponents because unicode does not provide a superscript solidus.
528      * Negative exponents can be used too.
529      * <p>
530      * These rules mean all the following (silly) examples are parsed properly:
531      * MHz, km/√d, kg.m.s⁻¹, µas^⅖/(h**(2)×m)³, km/√(kg.s), km**0.5, 2rev/d²
532      * </p>
533      * @param unitSpecification unit specification to parse
534      * @return parsed unit
535      */
536     public static Unit parse(final String unitSpecification) {
537 
538         // parse the specification
539         final List<PowerTerm> terms = Parser.buildTermsList(unitSpecification);
540 
541         if (terms == null) {
542             // special handling of "n/a"
543             return Unit.NONE;
544         }
545 
546         // build compound unit
547         Unit unit = Unit.ONE;
548         for (final PowerTerm term : terms) {
549             try {
550                 Unit u = PrefixedUnit.valueOf(term.getBase().toString());
551                 if (!Fraction.ONE.equals(term.getExponent())) {
552                     u = u.power(null, term.getExponent());
553                 }
554                 u = u.scale(null, term.getScale());
555                 unit = unit.multiply(null, u);
556             } catch (IllegalArgumentException iae) {
557                 throw new OrekitException(OrekitMessages.UNKNOWN_UNIT, term.getBase());
558             }
559         }
560 
561         // give final name to unit
562         return unit.alias(unitSpecification);
563 
564     }
565 
566     /** Check if the instance represents the same unit as another instance.
567      * <p>
568      * The name is not considered so aliases are considered equal.
569      * </p>
570      * @param unit other unit
571      * @return true if the instance and the other unit refer to the same unit
572      */
573     public boolean equals(final Object unit) {
574 
575         if (unit == this) {
576             // first fast check
577             return true;
578         }
579 
580         if (unit instanceof Unit) {
581             final Unit u = (Unit) unit;
582             return Precision.equals(scale, u.scale, 1) &&
583                    mass.equals(u.mass) && length.equals(u.length) && time.equals(u.time) &&
584                    current.equals(u.current) && angle.equals(u.angle);
585         }
586 
587         return false;
588 
589     }
590 
591     /** Get a hashcode for this unit.
592      * @return hashcode
593      */
594     public int hashCode() {
595         return 0x67e7 ^
596                (Double.hashCode(scale) << 12) ^
597                (mass.hashCode()        << 10) ^
598                (length.hashCode()      <<  8) ^
599                (time.hashCode()        <<  6) ^
600                (current.hashCode()     <<  4) ^
601                (angle.hashCode()       <<  2);
602     }
603 
604     /** {@inheritDoc} */
605     @Override
606     public String toString() {
607         return getName();
608     }
609 
610 }