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 }