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