untoldwind / KontrolSystem2

Autopilot scripting system for KSP2
Other
54 stars 14 forks source link

Off-Topic, Seeking help, Direction and vessel rotation. #141

Closed lefouvert closed 3 weeks ago

lefouvert commented 3 months ago

version 0.5.6.2 (Ckan) guessed tag : Question

Hi.

I'm sorry, it's not an issue with to2, it's a issue about me, but I don't really know where to ask and I'm pretty sure their is some more well lubricated and enlighten minds here able to help me.

The need :

I have computed 2 pairs of vectors. Each vector in a pair are perpendicular. One pair is Celestial Frame related, the other pair is Control Frame related. I need to rotate my vessel in order to make those two pair match.

The tools I have :

Where I'm stuck :

I'm not able to rotate the ship to make match pairs. When I move the pitch of my vessel, it don't necessarly make my difference pitch move for example. I understand why, but I'm not enough good in math to make rotate my reference in vessel control reference. (and since all my vector are vessel control referenced at the compute time, I'm not even sure why I'm not already in vessel control reference) I'm afraid about quaternions (even with thoses wonderfull explainations : 3Blue1Brown) (and anyway, the only way I see how to use this is to found the vector correponding to the axial center of rotation between my references and I'm not sure to know how to do that) and I would like to not have potential gimbal lock rotation matrice can have (and again, I'm not sure having knowledge to do that).


Here comes the drawing to illustrate what I'm saying :

Solarpanels


A bit of code if it can help :

Pair of vector from the vessel (ideal_exporure):

use { Vessel, Part, DeployableDeployState } from ksp::vessel
use { Vec3, vec3 } from ksp::math

// record of :
// vector going from command module to theorical ideal sun direction. Magnitude is expected EC output not considering blocking bodies or parts, or Star ratio/distance.
// vector to align with sun's first child normal to orbit
// vector are local to control frame
sync fn ideal_exposure(vessel: Vessel) -> (exposure: Vec3, normal: Vec3) = {
    const frame = vessel.control_frame

    // OUT: No operational solar panel.
// ksp::console::CONSOLE.print_line("OUT : No operational solar panel.")
    if (!vessel.parts.exists(fn(p) -> p.solar_panel.defined && p.deployable.value.deploy_state == DeployableDeployState.Extended)) // No operational solar panel
        return (exposure: vessel.global_up.to_local(frame), normal: vessel.global_facing.to_local(frame).vector)

    // All solar panels
    const solarPanelParts = vessel.parts
        .filter(fn(p) -> p.solar_panel.defined) // solar panel
        .filter(fn(p) -> p.deployable.value.deploy_state == DeployableDeployState.Extended)

    // Static solar panels
    const staticRotaries = group_statics_as_rotaries(solarPanelParts.filter(fn(p) -> !p.deployable.value.extendable))

    const trueStatic = solarPanelParts.filter(fn(p) -> !p.deployable.value.extendable)
        .filter(fn(ps) -> !staticRotaries.exists(fn(psr_arr) -> psr_arr.exists(fn(psr) -> ps.position == psr.position))) // TODO use part.id when available

    const staticLightVector = trueStatic
        .map(fn(p) -> - p.global_rotation.to_local(frame).right_vector.normalized * p.solar_panel.value.base_flow_rate) // coaxial to light vector, mag = max EC flow
        .reduce(vec3(0.0, 0.0, 0.0), fn(vsum, v) -> vsum + v)

    // Rotary solar panels. Normals to lightplanes are used as definition of planes.
    const lightRotaries = solarPanelParts.filter(fn(p) -> p.deployable.value.extendable)
        .map(fn(p) -> - p.global_rotation.to_local(frame).right_vector.normalized * p.solar_panel.value.base_flow_rate) // normal to light plane, mag = max EC flow
        + enlight_static_as_rotaries(staticRotaries)

    // OUT: No rotary. Process only statics.
// ksp::console::CONSOLE.print_line("OUT : No rotary. Process only statics.")
    if (lightRotaries.length == 0)
        return (exposure: staticLightVector, normal: random_normal(trueStatic))

    const lightPlanes = aggregate_rotaries_as_plane(lightRotaries)

    const intersectCount = (((lightPlanes.length - 1)**2) + (lightPlanes.length - 1)) / 2 // triangular number

    // OUT: Only 1 rotary. Process only statics + the rotary.
// ksp::console::CONSOLE.print_line("OUT : Only 1 rotary. Process only statics + the rotary.")
    if(intersectCount == 0) { // their is at least 1 plane. 0 plane case is already processed
        const rotaryLightVector =
            if(staticLightVector.magnitude > 0.0)
                (staticLightVector - (lightPlanes[Array.First].normalized * lightPlanes[Array.First].dot(staticLightVector))) // rotary light vector oriented mostly at static light vector (projection of static vector on rotary plane)
            else if(lightPlanes[Array.First].dot(vec3(1.0, 0.0, 0.0)) != lightPlanes[0].magnitude)
                lightPlanes[Array.First].cross(vec3(1.0, 0.0, 0.0))
            else
                lightPlanes[Array.First].cross(vec3(0.0, 1.0, 0.0))
        return (exposure: staticLightVector + rotaryLightVector, normal: staticLightVector.exclude_from(lightPlanes[Array.First] + random_normal(trueStatic)))
    }
// ksp::console::CONSOLE.print_line("intersectCount: " + intersectCount.to_string())

    // const intersectPlanes = (0..intersectCount)
    const intersectPlanes = (0..lightPlanes.length)
        .flat_map(fn(i) -> ((i + 1)..lightPlanes.length).map(fn(j) -> lightPlanes[i].cross(lightPlanes[j]).normalized * (lightPlanes[i].magnitude + lightPlanes[j].magnitude)))
        //.flat_map(fn(arr_intersect) -> arr_intersect) // intersect plane is best direction to light both light plane. Sum up all intersect.
        // .reduce(<Vec3>[], fn(flat, arr_intersect) -> flat + arr_intersect) // intersect plane is best direction to light both light plane. Sum up all intersect.
// ksp::console::CONSOLE.print_line("intersectPlanes.lenght: " + intersectPlanes.length.to_string())

    const allLightVector = fasten_sum_unoriented_vectors(intersectPlanes, staticLightVector) + staticLightVector
    const allLightVectorNormal = allLightVector.exclude_from(fasten_sum_unoriented_vectors(lightPlanes, staticLightVector))

    // OUT: All.
// ksp::console::CONSOLE.print_line("OUT : All.")
    (exposure: allLightVector, normal: allLightVectorNormal)
}

// TODO externalize anonym function into named function in order to make code easier to read. As it should be more understandable, get rid of comments.
sync fn group_statics_as_rotaries(staticSolarPanels: Part[]) -> Part[][] = {
    if(staticSolarPanels.length < 2)
        return [<Part>[]]

    const accuracy = 2

    (0..staticSolarPanels.length)
        .flat_map(fn(i) -> ((i + 1)..staticSolarPanels.length).map(fn(j) -> (
            partA: staticSolarPanels[i],
            partB: staticSolarPanels[j],
            normal: (- staticSolarPanels[i].global_rotation.right_vector)
                .cross(- staticSolarPanels[j].global_rotation.right_vector)
                .normalized * (staticSolarPanels[i].solar_panel.value.base_flow_rate + staticSolarPanels[j].solar_panel.value.base_flow_rate))
        ))//.flat_map(fn(arr) -> arr) // record of all parts'pair normal registred in a flat array
        .reduce(<(parts: Part[], normalId: GlobalVector)>[], fn(groups, partPair) -> { // group up parts with same normal
            if(groups.length == Array.Empty) // init
                return [(parts: [partPair.partA, partPair.partB], normalId: partPair.normal)]
            if(groups.exists(fn(g) -> almost_equal_global(g.normalId, partPair.normal, partPair.partA.vessel.control_frame, accuracy))) // grow
                return groups.map(fn(g) -> {
                    if(almost_equal_global(g.normalId, partPair.normal, partPair.partA.vessel.control_frame, accuracy)) { // to group up
                        if(g.parts.exists(fn(p) -> p.position == partPair.partA.position) && (!g.parts.exists(fn(p) -> p.position == partPair.partB.position))) // TODO prefer a part.id than a part.position when it will be available
                            return (parts: g.parts + partPair.partB, normalId: g.normalId)
                        if(g.parts.exists(fn(p) -> p.position == partPair.partB.position) && (!g.parts.exists(fn(p) -> p.position == partPair.partA.position))) // TODO prefer a part.id than a part.position when it will be available
                            return (parts: g.parts + partPair.partA, normalId: g.normalId)
                        return g
                    } else // not concerned
                        g
                })
            groups + (parts: [partPair.partA, partPair.partB], normalId: partPair.normal) // new
        }).map(fn(r) -> r.parts) // get rid of normals
        .filter(fn(a) -> round_at(a.reduce(vec3(0.0, 0.0, 0.0), fn(v, p) -> v + ((- p.global_rotation.to_local(p.vessel.control_frame).right_vector) * p.solar_panel.value.base_flow_rate)).magnitude, accuracy) == 0.0) // keep groups where (sum vec).magnitude == 0.0
        .sort_by(fn(a) -> a.length)
        .reverse()
        .reduce(<Part[]>[], fn(clean, partArr) -> {
            if(clean.length == Array.Empty)
                return clean + partArr
            return clean + partArr.filter(fn(part) -> !clean.flat_map(fn(a) -> a).exists(fn(p) -> p.position == part.position)) // TODO prefer a part.id than a part.position when it will be available
        }) // deduplicate
        .filter(fn(a) -> a.length > Array.Empty) // get rid of possible empties array
}

sync fn enlight_static_as_rotaries(staticSolarPanels: Part[][]) -> Vec3[] =
    staticSolarPanels
        .map(fn(circle) -> {
            const vessel = circle[Array.First].vessel
            const frame = vessel.control_frame
            const accuracy = 2

            const meanFlowRate = (circle.reduce(0.0, fn(sum, p) -> sum + p.solar_panel.value.base_flow_rate) / circle.length.to_float) * (Circle.Quarter / Circle.Full) // ((sum individual flow) / part_number) * mean_exposure

            const legitimAxis = (- circle[Array.First].global_rotation.to_local(frame).right_vector)
                .cross(- circle[circle.length - 1].global_rotation.to_local(frame).right_vector)
                .normalized * meanFlowRate
            if(round_at(legitimAxis.magnitude, accuracy) == 0.0)
                return random_normal(circle)
            legitimAxis
        })

sync fn random_normal(staticSolarPanels: Part[]) -> Vec3 = {
    const staticLightVector = staticSolarPanels
        .map(fn(p) -> - p.global_rotation.to_local(p.vessel.control_frame).right_vector.normalized * p.solar_panel.value.base_flow_rate) // coaxial to light vector, mag = max EC flow
        .reduce(vec3(0.0, 0.0, 0.0), fn(vsum, v) -> vsum + v)
    const staticLightVectorNormalCase0 = staticSolarPanels
        .reduce(vec3(0.0, 0.0, 0.0), fn(sum, p) -> sum + p.global_rotation.to_local(p.vessel.control_frame).up_vector.normalized * p.solar_panel.value.base_flow_rate)
    const staticLightVectorNormalCase1 = staticSolarPanels
        .reduce(vec3(0.0, 0.0, 0.0), fn(sum, p) -> sum + p.global_rotation.to_local(p.vessel.control_frame).vector.normalized * p.solar_panel.value.base_flow_rate)
    staticLightVector.exclude_from(if(staticLightVectorNormalCase0.magnitude > staticLightVectorNormalCase1.magnitude) staticLightVectorNormalCase0 else staticLightVectorNormalCase1)
}

// sum parallels planes
sync fn aggregate_rotaries_as_plane(lightRotaries: Vec3[]) -> Vec3[] =
    lightRotaries.reduce(<Vec3>[], fn(notParallel, normal) -> {
        if(!notParallel.exists(fn(np) -> round_at(np.normalized.cross(normal.normalized).magnitude, 2) == 0.0)) // empty or not parallel
            return notParallel + normal
        return notParallel.map(fn(np) -> {
            if(round_at(np.cross(normal).magnitude, 1) == 0.0) // sum up all parallel planes
                np + (np.normalized.dot(normal.normalized) * normal) // force normal to be in the same direction than np
            else
                np
        })
    })

sync fn fasten_sum_unoriented_vectors(unorientedVectors: Vec3[], reference: Vec3) -> Vec3 =
    unorientedVectors.reduce(vec3(0.0, 0.0, 0.0), fn(vsum, v) -> {
        if(round_at(reference.normalized.dot(v.normalized), 1) != 0.0) // mostly in direction of reference, if any
            vsum + (v * (reference.normalized.dot(v.normalized)))
        else
            vsum + v
    })

Pair of vector from orbital view (starVector and orbitNormal) :

        let referenceOrbit = vessel.orbit
        while(referenceOrbit.reference_body.parent_body.defined)
            referenceOrbit = referenceOrbit.reference_body.orbit
        const starVector = referenceOrbit.reference_body.global_position.to_local(celframe)
        const orbitNormal = referenceOrbit.orbit_normal

Difference between direction (deltaDir) :

            const bestExposure = ideal_exposure(vessel)
            const starDir = ksp::math::look_dir_up(starVector.to_global(vessel.celestial_frame).to_local(vessel.control_frame), orbitNormal.to_global(vessel.celestial_frame).to_local(vessel.control_frame))
            const exposureDir = ksp::math::look_dir_up(bestExposure.exposure, bestExposure.normal)
            const deltaDir = starDir - exposureDir

custom tools :

use { round } from core::math
use { Vec3, vec3, GlobalVector, TransformFrame } from ksp::math

pub type CircleConst = float

pub const Circle: (None: CircleConst, Quarter: CircleConst, Semi: CircleConst, ThreeQuarter: CircleConst, Full: CircleConst) = (
    None: 0.0,
    Quarter: 90.0,
    Semi: 180.0,
    ThreeQuarter: 270.0,
    Full: 360.0
)

pub type ArrayConst = int

pub const Array: (Empty: ArrayConst, First: ArrayConst) = (
    Empty: 0,
    First: 0
)

pub sync fn round_at(value: float, decimals: int) -> float =
    (round(value * (10**decimals)) / (10**decimals))

pub sync fn almost_equal_global(vectorA: GlobalVector, vectorB: GlobalVector, frame: TransformFrame, accuracy: int) -> bool =
    almost_equal(vectorA.to_local(frame), vectorB.to_local(frame), accuracy)

pub sync fn almost_equal(vectorA: Vec3, vectorB: Vec3, accuracy: int) -> bool = (
    round_at(vectorA.x, accuracy) == round_at(vectorB.x, accuracy)
    && round_at(vectorA.y, accuracy) == round_at(vectorB.y, accuracy)
    && round_at(vectorA.z, accuracy) == round_at(vectorB.z, accuracy)
)

So right now, you guys are taxidrivers having a redhead, panties only 'lefouvert' going through the roof of your cab whining «PleEaaAse HeEelp».

untoldwind commented 3 months ago

A bit of code if it can help :

Quite the understatement :rofl:

Let me see if I understand this right:

lefouvert commented 3 months ago

Thoses exemple comes from the git, it's not up to date and don't exactly match what I'm saying in this post ;)

But yes, ideal_exposure returns a vector in the control frame of the vessel that point in the direction the sun-light should come in to maximize the solar power generation. It also return a vector which decribe the best axial rotation if the vessel never correct it's position, in order to have a «good enough» exposition in monthly scale time line. And the little drawing which come with, to illustrate what about the normal is. In the case of this drawing, ideal_exposure(vessel).normal is the axle of the rotary solar panel. Solarpanels1


const bestDir = ideal_exposure(vessel).normal.to_global(vessel.control_frame) should be the global variant of this.

Outdated but true.

const currentDir = (vessel.global_posistion - star.position).normalized is the direction the sun-light is currently coming in.

Outdated, but still true. In this current post, it refere to starVector

const rotation = ksp::math::global_from_vector_to_vector(currentDir, bestDir) should give a rotation that transforms currentDir to bestDir ... which should be the rotation the vessel has to perform

Still outdated, but it's what I hoped. My concern about this line of code is the loss of normal consideration, and how it compute the roll. In this current post, I tried to compute direction from both vectors pair in order to keep normal considerations. const starDir = ksp::math::look_dir_up(starVector.to_global(vessel.celestial_frame).to_local(vessel.control_frame), orbitNormal.to_global(vessel.celestial_frame).to_local(vessel.control_frame)) for actual sun position and the normal I want to follow const exposureDir = ksp::math::look_dir_up(bestExposure.exposure, bestExposure.normal) for the vector I want to point in direction of sun, and the vector I want to point in same direction than «the normal I want to follow» Then, const deltaDir = starDir - exposureDir became, I think a better representation of what i've called rotation from the git.

If that is applied to vessel.global_facing.vector (i.e. rotation * vessel.global_facing.vector) the result should be a global vector the vessel should point to ... which then could be fed into the SAS (or other methods of steering)

Hopefully, but it don't work. SAS can't be an option since it don't care about roll. So «other method» it will be. @untoldwind had wrote a very nice pid controller std::control::steering::control_steering(Vessel) you can initialize with 3 directions variants (global, celestial frame, horizon frame), pause and resume but I'm still not exactly sure how it work since I'm not sure how about to interpret deltaDir (The direction which is equal to R[0.0, 0.0, 0.0] when I'm well rotated)

Conclusion : And as I'm stubborn, I would like to try to do my own lightweight interpretation of steering, for the pleasure. But it comes far after being able to rotate my vessel the way I want. Even if all my statics tests on the launchpad and the dozen of debug vectors I draw in orbit seems to confirm than ideal_exposure work as intended, I would like to see it in action. And anyway, if it work, but I'm not able to use it, it's pointless.

untoldwind commented 3 months ago

... I knew I forgot something :)

The std::control::steering::control_steering(Vessel) should be able to orient the vessel according to the rotation.up_vector. But using vector_to_vector probably wont give you the right one.

An initial solution might be to first use vector_to_vector to point the vessel in the right direction ignoring the roll. Then transform the local ideal_exposure to the new vessel.control_frame and use the angles between starDir, ideal_exposure and vessel.global_up to adjust the roll. Not perfect, but something to improve upon.

Edit: The std::control::steering controller is actually a slightly adjusted variant of the kOS SteeringManager ... as a recall the adjusting all the rotations to the correct reference frame was quite painful as well ;)

lefouvert commented 3 months ago

I should update the git ^^

@untoldwind As you said, I'm afraid vector_to_vector wont give me the right one. Do you think deltaDir.up_vector (from this post, not from the git) is the right one since I tried to not lost information in this Direction ?

I will try the «2 acts - refFrame change» rotation

And all the same, adaptation or not, it's still a nice piece of work ^^ Anyway, at best, we're reinventing the wheel, or trying to update existing work. Pure creation out of nowhere on untrodden path is what make history, and I don't think anyone of us is trying to make history (Even with KSP1 «Making History Expansion» DLC ^^)

untoldwind commented 3 months ago

Actually it is way simpler (funny what happens to the brain if you sleep over it).

General idea: const rot = global_vector_to_vector(desired_point_to_sun, current_point_to_sun) is the rotation the vessel has to perform. vessel.global_facing is the current direction/rotation of the vessel, so rot * vessel.global_facing should be the new one. (You do care about the roll around the vessel axis, but not so much about the "roll" round the point-to-sun axis)

Little test script:

use { Vessel } from ksp::vessel
use { CONSOLE, BLUE, RED, YELLOW } from ksp::console
use { DEBUG } from ksp::debug
use { vec3, global_from_vector_to_vector } from ksp::math
use { sleep, wait_until } from ksp::game
use { find_body } from ksp::orbit
use { control_steering } from std::control::steering

pub fn main_flight(vessel: Vessel) -> Result<Unit, string> = {
    CONSOLE.clear()

    const pointToSunCf = vec3(1, 2,3).normalized // Anything will do

    DEBUG.clear_markers()
    DEBUG.add_vector(fn() -> vessel.global_position, fn() -> 20 * vessel.global_facing.vector, BLUE, "Forward", 2)
    DEBUG.add_vector(fn() -> vessel.global_position, fn() -> 20 * vessel.global_facing.up_vector, RED, "Up", 2)
    DEBUG.add_vector(fn() -> vessel.global_position, fn() -> 20 * pointToSunCf.to_global(vessel.control_frame), YELLOW, "To the sun", 2)

    const sun = find_body("Kerbol")?

    const rotation = global_from_vector_to_vector(pointToSunCf.to_global(vessel.control_frame), (sun.global_position - vessel.global_position).normalized)

    const steering = control_steering(vessel)

    // set_global_direction has an issue I am investigating
    steering.set_direction((rotation * vessel.global_facing).to_local(vessel.main_body.celestial_frame))

    CONSOLE.print_line("Waiting")
    while(pointToSunCf.to_global(vessel.control_frame) * (sun.global_position - vessel.global_position).normalized < 0.97) {
        steering.controller.print_debug()
        sleep(0.1)
    }

    CONSOLE.print_line("Done")

    sleep(10000) // Sleep do admire the vectors
}

Edit: Concerning the steering.set_global_direction ... turns out that rotation internally carries around the control-frame of the vessel, which of course is rotating with the vessel while steering, so converting it to a non-rotating frame (like .body.celestial_frame) is really important. I guess this should be done internally in global_from_vector_to_vector

lefouvert commented 3 months ago

You can't imagine how many issue I had solved with a good sleep. At work, I'm known for «The guy who say often "I don't have a solution, I'll take a nap and I'll come back with something."». And personaly, I've been awaken when a solution hit me like a truck in my sleep at 3AM too many times. After, I have to write it to not forget, and can sleep as a baby right after.

But did you take a look to the «not git version, but the post -here- version» with deltaDir ? As I'm very bad with rotation (Not only mathmaticly, but «IRL» too. I'm able to lost myself in a straigh street.), I'm not able to describe the difference between rotation and deltaDir.

Anyway, as soon my graphic card became available (I had launch a batch of heavy computing on it), I take a try.

About the Edit : I have noticed than frames are caried sometimes. Here for example : Even if exposureDefinitionVesFrame is a constant, it will follow the vessel while exposureDefinition don't

        const celframe = vessel.celestial_frame
        const vesframe = vessel.control_frame

        const exposureDefinitionVesFrame = ideal_exposure(self.ship)
        const exposureDefinition = (exposure: exposureDefinitionVesFrame.exposure.to_global(vesframe).to_local(celframe), normal: exposureDefinitionVesFrame.normal.to_global(vesframe).to_local(celframe))

ksp::debug::DEBUG.add_vector(
    fn() -> vessel.command_modules[Array.First].part.global_position,
    fn() -> exposureDefinition.exposure.to_global(celframe).normalized * 25,
    ui::constant::ULTRAMARINE,
    "initial best exposure",
    2)

ksp::debug::DEBUG.add_vector(
    fn() -> vessel.command_modules[Array.First].part.global_position,
    fn() -> exposureDefinition.normal.to_global(celframe).normalized * 25,
    ui::constant::GREY,
    "initial best normal",
    2)

ksp::debug::DEBUG.add_vector(
    fn() -> vessel.command_modules[Array.First].part.global_position,
    fn() -> exposureDefinitionVesFrame.exposure.to_global(celframe).normalized * 25,
    ui::constant::ULTRAMARINE,
    "best exposure",
    2)

ksp::debug::DEBUG.add_vector(
    fn() -> vessel.command_modules[Array.First].part.global_position,
    fn() -> exposureDefinitionVesFrame.normal.to_global(celframe).normalized * 25,
    ui::constant::GREY,
    "best normal",
    2)
untoldwind commented 3 months ago

Re-framing it to the celestial frame of the body works for me:

const frame = vessel.main_body.celestial_frame
dir.to_local(frame).to_global(frame)

I tried to do this internally by re-framing every global rotation to the celestial frame of the galactic origin, but this resulted in some very wired results. So for now the above code chunk is probably the best workaround

untoldwind commented 3 months ago

To pick up the original discussion: I think the starDir - exposureDir will produce a similar rotation/direction as the vector_to_vector ... just with a different "roll" around the axis towards the star, which should not matter.

To use this to the steering_manager it should be converted to the celestial_frame as described above (until there is a better solution), in which case steering.set_direction can be used. To get the desired target direction of the vessel you can simply multiply deltaDir * vessel.facing

lefouvert commented 3 months ago

I almost have what I expect. When it will be done, I'll share the code. I don't know If I will be able to share a nice code or a «how it work ?» code. I get at least one error source : my vessel was rotating at measure time. It introduce HUGE error (at least 20°)

I have a weird behavior : I launch my test script, it rotate, it's not good but it's better, I force close. I launch it again, it again better I force close etc Sometimes, I have to launch it 3 times in row to have expected result. The result is really good, but not at the first time. I'll probably investigate sunday night.

Side notes : roll matters because I want to be able to park and forget. So aTTitude correction maneuvers are not intended as soon as a park maneuver has been done. What could be the exposure in 3 or 6 month (in game time) if I don't correct the attitude ? That's why roll import, bestExposure.normal matching orbitNormal is expected to garantee a good enough exposure in case of presence of rotary panels in time. Refer to this scheme to see what I mean.

untoldwind commented 3 months ago

Without some deep debugging I can be certain, but this sounds like what happens when a frame of reference shifts over time.

The global vectors/positions and transformation hierarchy that was introduced with KSP2 is really helpful in many ways, but unluckily it is not stable in time (obviously). I guess the indented to use the global vectors just for some calculations during a game update-cycle and then throw them away.

A good rule of thumb (for now?):

Maybe it is worth a separate discussion if and how the current bindings should change. I have some rough ideas, but they might result in an API that is even more confusing than the current one. There are so many use-cases to consider:

lefouvert commented 3 months ago

Hum... I get something. I dont understand why, I don't understand how it works (and I hate this), but it works. It «auto-retry» until it reach the goal. First atempt is always a mess.

I can barely work on it to make it better since I don't understand why it have to make multiple atempts to get the point. I assume it's because I work on a moving frame (In my test, it correspond to find_body("Kerbin").value.celestial_frame))

Here the code (all is included, no need for custom out references): It's ugly, I can rewrite it in a better way, refactor it, but at the time, it's the only workable code I have.

use { Vessel } from ksp::vessel
// use { CONSOLE, BLUE, RED, YELLOW } from ksp::console
use { CONSOLE } from ksp::console
use { DEBUG } from ksp::debug
use { vec3, global_from_vector_to_vector, global_look_dir_up, look_dir_up } from ksp::math
use { sleep, wait_until } from ksp::game
use { find_body } from ksp::orbit
use { control_steering } from std::control::steering

pub fn main_flight(vessel: Vessel) -> Result<Unit, string> = {
    CONSOLE.clear()

    // const staticFrame = vessel.celestial_frame // not a good frame. At each while(!done()) loop, does some gruesome alignment, and never get the goal.
    const staticFrame = vessel.main_body.celestial_frame // Ok but need at least 3 atempts to reach the goal

    // In case of interstellar flight, the star could be something other than 'Kerbol'
    let referenceOrbit = vessel.orbit
    while(referenceOrbit.reference_body.parent_body.defined)
        referenceOrbit = referenceOrbit.reference_body.orbit

    // const staticFrame = referenceOrbit.reference_body.celestial_frame // Meh tier frame. some datas are hard to get (as aligned), slow, but I get a good result at the end.
    const starVector = referenceOrbit.reference_body.global_position.to_local(staticFrame)
    const orbitNormal = referenceOrbit.orbit_normal

    const exposureDefinitionVesFrame = ideal_exposure(vessel)
    const initialExposureDefinition = (exposure: exposureDefinitionVesFrame.exposure.to_global(vessel.control_frame).to_local(staticFrame), normal: exposureDefinitionVesFrame.normal.to_global(vessel.control_frame).to_local(staticFrame))

    const initialFacing = vessel.global_facing.vector
    const initialFacingDir = vessel.global_facing

    DEBUG.clear_markers()
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> initialExposureDefinition.exposure.to_global(staticFrame).normalized * 25, ULTRAMARINE, "initial best exposure", 2)
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> initialExposureDefinition.normal.to_global(staticFrame).normalized * 25, GREY, "initial best normal", 2)
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> initialFacing.normalized * 25, CRIMSON, "initial facing", 2)
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> exposureDefinitionVesFrame.normal.to_global(vessel.control_frame).normalized * 50, WHITE, "best normal", 1)
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> exposureDefinitionVesFrame.exposure.to_global(vessel.control_frame).normalized * 50, BLUE, "best exposure", 1)
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> vessel.global_facing.vector * 50, RED, "facing", 1)
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> orbitNormal.to_global(staticFrame).normalized * 75, GREEN, "ObtNormal", 0.5)
    DEBUG.add_vector(fn() -> vessel.command_modules[Array.First].part.global_position, fn() -> starVector.to_global(staticFrame).normalized * 75, YELLOW, "Kerbol", 0.5)

    const steering = control_steering(vessel)
    CONSOLE.print_line("Waiting")
    ksp::console::CONSOLE.print_at(30, 0, "aligned : " + (exposureDefinitionVesFrame.exposure.to_global(vessel.control_frame).normalized * starVector.to_global(staticFrame).normalized).to_string())
    ksp::console::CONSOLE.print_at(32, 0, "facing : " + (vessel.global_facing).to_local(staticFrame).to_string())

    const done = fn() -> exposureDefinitionVesFrame.exposure.to_global(vessel.control_frame).normalized * starVector.to_global(staticFrame).normalized > 0.999
        && exposureDefinitionVesFrame.normal.to_global(vessel.control_frame).normalized * orbitNormal.to_global(staticFrame).normalized > 0.999

    const starVectorDir = look_dir_up(starVector.normalized, orbitNormal.normalized)

    while(!done()) {

        const exposureDefinitionRecalibrate = (exposure: exposureDefinitionVesFrame.exposure.to_global(vessel.control_frame).to_local(staticFrame), normal: exposureDefinitionVesFrame.normal.to_global(vessel.control_frame).to_local(staticFrame))

        const pointToSunCfDir = look_dir_up(exposureDefinitionRecalibrate.exposure.normalized, exposureDefinitionRecalibrate.normal.normalized)
        const rotation = (starVectorDir * vessel.facing) - (pointToSunCfDir * vessel.facing)

        steering.set_direction(rotation * vessel.facing)

        while(vessel.angular_velocity.magnitude > 0.0005){
            steering.controller.print_debug()
            CONSOLE.print_at(30, 0, "rotation : " + (rotation * vessel.facing).to_string())
            CONSOLE.print_at(31, 0, "facing   : " + (vessel.global_facing).to_local(staticFrame).to_string())
            CONSOLE.print_at(33, 0, "aligned S: " + (exposureDefinitionVesFrame.exposure.to_global(vessel.control_frame).normalized * starVector.to_global(staticFrame).normalized).to_string())
            CONSOLE.print_at(34, 0, "aligned O: " + (exposureDefinitionVesFrame.normal.to_global(vessel.control_frame).normalized * orbitNormal.to_global(staticFrame).normalized).to_string())
            CONSOLE.print_at(35, 0, "ang_vel  : " + vessel.angular_velocity.magnitude.to_string())
            CONSOLE.print_at(37, 0, "semi done: " + (!(vessel.angular_velocity.magnitude > 0.0005)).to_string() + " ")
            CONSOLE.print_at(38, 0, "full done: " + done().to_string() + " ")
            sleep(0.1)
        }

        CONSOLE.print_at(37, 0, "semi done: " + (!(vessel.angular_velocity.magnitude > 0.0005)).to_string())
        CONSOLE.print_at(38, 0, "full done: " + done().to_string())
        sleep(1)
    }

    CONSOLE.clear()
    CONSOLE.print_line("Done")
    steering.release()
    sleep(10000) // Sleep do admire the vectors -> it's pretty satisfiying when it works.
}

// my stuff in order to not have to import them in case of sharing.
use { Part, DeployableDeployState } from ksp::vessel
use { Vec3, GlobalVector, TransformFrame } from ksp::math

use { round } from core::math

pub type CircleConst = float

pub const Circle: (None: CircleConst, Quarter: CircleConst, Semi: CircleConst, ThreeQuarter: CircleConst, Full: CircleConst) = (
    None: 0.0,
    Quarter: 90.0,
    Semi: 180.0,
    ThreeQuarter: 270.0,
    Full: 360.0
)

pub type ArrayConst = int

pub const Array: (Empty: ArrayConst, First: ArrayConst) = (
    Empty: 0,
    First: 0
)

pub sync fn round_at(value: float, decimals: int) -> float =
    (round(value * (10**decimals)) / (10**decimals))

pub sync fn almost_equal_global(vectorA: GlobalVector, vectorB: GlobalVector, frame: TransformFrame, accuracy: int) -> bool =
    almost_equal(vectorA.to_local(frame), vectorB.to_local(frame), accuracy)

pub sync fn almost_equal(vectorA: Vec3, vectorB: Vec3, accuracy: int) -> bool = (
    round_at(vectorA.x, accuracy) == round_at(vectorB.x, accuracy)
    && round_at(vectorA.y, accuracy) == round_at(vectorB.y, accuracy)
    && round_at(vectorA.z, accuracy) == round_at(vectorB.z, accuracy)
)

sync fn ideal_exposure(vessel: Vessel) -> (exposure: Vec3, normal: Vec3) = {
    const frame = vessel.control_frame

    // OUT: No operational solar panel.
// ksp::console::CONSOLE.print_line("OUT : No operational solar panel.")
    if (!vessel.parts.exists(fn(p) -> p.solar_panel.defined && p.deployable.value.deploy_state == DeployableDeployState.Extended)) // No operational solar panel
        return (exposure: vessel.global_up.to_local(frame), normal: vessel.global_facing.to_local(frame).vector)

    // All solar panels
    const solarPanelParts = vessel.parts
        .filter(fn(p) -> p.solar_panel.defined) // solar panel
        .filter(fn(p) -> p.deployable.value.deploy_state == DeployableDeployState.Extended)

    // Static solar panels
    const staticRotaries = group_statics_as_rotaries(solarPanelParts.filter(fn(p) -> !p.deployable.value.extendable))

    const trueStatic = solarPanelParts.filter(fn(p) -> !p.deployable.value.extendable)
        .filter(fn(ps) -> !staticRotaries.exists(fn(psr_arr) -> psr_arr.exists(fn(psr) -> ps.position == psr.position))) // TODO use part.id when available

    const staticLightVector = trueStatic
        .map(fn(p) -> - p.global_rotation.to_local(frame).right_vector.normalized * p.solar_panel.value.base_flow_rate) // coaxial to light vector, mag = max EC flow
        .reduce(vec3(0.0, 0.0, 0.0), fn(vsum, v) -> vsum + v)

    // Rotary solar panels. Normals to lightplanes are used as definition of planes.
    const lightRotaries = solarPanelParts.filter(fn(p) -> p.deployable.value.extendable)
        .map(fn(p) -> - p.global_rotation.to_local(frame).right_vector.normalized * p.solar_panel.value.base_flow_rate) // normal to light plane, mag = max EC flow
        + enlight_static_as_rotaries(staticRotaries)

    // OUT: No rotary. Process only statics.
// ksp::console::CONSOLE.print_line("OUT : No rotary. Process only statics.")
    if (lightRotaries.length == 0)
        return (exposure: staticLightVector, normal: random_normal(trueStatic))

    const lightPlanes = aggregate_rotaries_as_plane(lightRotaries)

    const intersectCount = (((lightPlanes.length - 1)**2) + (lightPlanes.length - 1)) / 2 // triangular number

    // OUT: Only 1 rotary. Process only statics + the rotary.
// ksp::console::CONSOLE.print_line("OUT : Only 1 rotary. Process only statics + the rotary.")
    if(intersectCount == 0) { // their is at least 1 plane. 0 plane case is already processed
        const rotaryLightVector =
            if(staticLightVector.magnitude > 0.0)
                (staticLightVector - (lightPlanes[Array.First].normalized * lightPlanes[Array.First].dot(staticLightVector))) // rotary light vector oriented mostly at static light vector (projection of static vector on rotary plane)
            else if(lightPlanes[Array.First].dot(vec3(1.0, 0.0, 0.0)) != lightPlanes[0].magnitude)
                lightPlanes[Array.First].cross(vec3(1.0, 0.0, 0.0))
            else
                lightPlanes[Array.First].cross(vec3(0.0, 1.0, 0.0))
        return (exposure: staticLightVector + rotaryLightVector, normal: staticLightVector.exclude_from(lightPlanes[Array.First] + random_normal(trueStatic)))
    }
// ksp::console::CONSOLE.print_line("intersectCount: " + intersectCount.to_string())

    // const intersectPlanes = (0..intersectCount)
    const intersectPlanes = (0..lightPlanes.length)
        .flat_map(fn(i) -> ((i + 1)..lightPlanes.length).map(fn(j) -> lightPlanes[i].cross(lightPlanes[j]).normalized * (lightPlanes[i].magnitude + lightPlanes[j].magnitude)))
        //.flat_map(fn(arr_intersect) -> arr_intersect) // intersect plane is best direction to light both light plane. Sum up all intersect.
        // .reduce(<Vec3>[], fn(flat, arr_intersect) -> flat + arr_intersect) // intersect plane is best direction to light both light plane. Sum up all intersect.
// ksp::console::CONSOLE.print_line("intersectPlanes.lenght: " + intersectPlanes.length.to_string())

    const allLightVector = fasten_sum_unoriented_vectors(intersectPlanes, staticLightVector) + staticLightVector
    const allLightVectorNormal = allLightVector.exclude_from(fasten_sum_unoriented_vectors(lightPlanes, staticLightVector))

    // OUT: All.
// ksp::console::CONSOLE.print_line("OUT : All.")
    (exposure: allLightVector, normal: allLightVectorNormal)
}

// TODO externalize anonym function into named function in order to make code easier to read. As it should be more understandable, get rid of comments.
sync fn group_statics_as_rotaries(staticSolarPanels: Part[]) -> Part[][] = {
    if(staticSolarPanels.length < 2)
        return [<Part>[]]

    const accuracy = 2

    (0..staticSolarPanels.length)
        .flat_map(fn(i) -> ((i + 1)..staticSolarPanels.length).map(fn(j) -> (
            partA: staticSolarPanels[i],
            partB: staticSolarPanels[j],
            normal: (- staticSolarPanels[i].global_rotation.right_vector)
                .cross(- staticSolarPanels[j].global_rotation.right_vector)
                .normalized * (staticSolarPanels[i].solar_panel.value.base_flow_rate + staticSolarPanels[j].solar_panel.value.base_flow_rate))
        ))//.flat_map(fn(arr) -> arr) // record of all parts'pair normal registred in a flat array
        .reduce(<(parts: Part[], normalId: GlobalVector)>[], fn(groups, partPair) -> { // group up parts with same normal
            if(groups.length == Array.Empty) // init
                return [(parts: [partPair.partA, partPair.partB], normalId: partPair.normal)]
            if(groups.exists(fn(g) -> almost_equal_global(g.normalId, partPair.normal, partPair.partA.vessel.control_frame, accuracy))) // grow
                return groups.map(fn(g) -> {
                    if(almost_equal_global(g.normalId, partPair.normal, partPair.partA.vessel.control_frame, accuracy)) { // to group up
                        if(g.parts.exists(fn(p) -> p.position == partPair.partA.position) && (!g.parts.exists(fn(p) -> p.position == partPair.partB.position))) // TODO prefer a part.id than a part.position when it will be available
                            return (parts: g.parts + partPair.partB, normalId: g.normalId)
                        if(g.parts.exists(fn(p) -> p.position == partPair.partB.position) && (!g.parts.exists(fn(p) -> p.position == partPair.partA.position))) // TODO prefer a part.id than a part.position when it will be available
                            return (parts: g.parts + partPair.partA, normalId: g.normalId)
                        return g
                    } else // not concerned
                        g
                })
            groups + (parts: [partPair.partA, partPair.partB], normalId: partPair.normal) // new
        }).map(fn(r) -> r.parts) // get rid of normals
        .filter(fn(a) -> round_at(a.reduce(vec3(0.0, 0.0, 0.0), fn(v, p) -> v + ((- p.global_rotation.to_local(p.vessel.control_frame).right_vector) * p.solar_panel.value.base_flow_rate)).magnitude, accuracy) == 0.0) // keep groups where (sum vec).magnitude == 0.0
        .sort_by(fn(a) -> a.length)
        .reverse()
        .reduce(<Part[]>[], fn(clean, partArr) -> {
            if(clean.length == Array.Empty)
                return clean + partArr
            return clean + partArr.filter(fn(part) -> !clean.flat_map(fn(a) -> a).exists(fn(p) -> p.position == part.position)) // TODO prefer a part.id than a part.position when it will be available
        }) // deduplicate
        .filter(fn(a) -> a.length > Array.Empty) // get rid of possible empties array
}

sync fn enlight_static_as_rotaries(staticSolarPanels: Part[][]) -> Vec3[] =
    staticSolarPanels
        .map(fn(circle) -> {
            const vessel = circle[Array.First].vessel
            const frame = vessel.control_frame
            const accuracy = 2

            const meanFlowRate = (circle.reduce(0.0, fn(sum, p) -> sum + p.solar_panel.value.base_flow_rate) / circle.length.to_float) * (Circle.Quarter / Circle.Full) // ((sum individual flow) / part_number) * mean_exposure

            const legitimAxis = (- circle[Array.First].global_rotation.to_local(frame).right_vector)
                .cross(- circle[circle.length - 1].global_rotation.to_local(frame).right_vector)
                .normalized * meanFlowRate
            if(round_at(legitimAxis.magnitude, accuracy) == 0.0)
                return random_normal(circle)
            legitimAxis
        })

sync fn random_normal(staticSolarPanels: Part[]) -> Vec3 = {
    const staticLightVector = staticSolarPanels
        .map(fn(p) -> - p.global_rotation.to_local(p.vessel.control_frame).right_vector.normalized * p.solar_panel.value.base_flow_rate) // coaxial to light vector, mag = max EC flow
        .reduce(vec3(0.0, 0.0, 0.0), fn(vsum, v) -> vsum + v)
    const staticLightVectorNormalCase0 = staticSolarPanels
        .reduce(vec3(0.0, 0.0, 0.0), fn(sum, p) -> sum + p.global_rotation.to_local(p.vessel.control_frame).up_vector.normalized * p.solar_panel.value.base_flow_rate)
    const staticLightVectorNormalCase1 = staticSolarPanels
        .reduce(vec3(0.0, 0.0, 0.0), fn(sum, p) -> sum + p.global_rotation.to_local(p.vessel.control_frame).vector.normalized * p.solar_panel.value.base_flow_rate)
    staticLightVector.exclude_from(if(staticLightVectorNormalCase0.magnitude > staticLightVectorNormalCase1.magnitude) staticLightVectorNormalCase0 else staticLightVectorNormalCase1)
}

// sum parallels planes
sync fn aggregate_rotaries_as_plane(lightRotaries: Vec3[]) -> Vec3[] =
    lightRotaries.reduce(<Vec3>[], fn(notParallel, normal) -> {
        if(!notParallel.exists(fn(np) -> round_at(np.normalized.cross(normal.normalized).magnitude, 2) == 0.0)) // empty or not parallel
            return notParallel + normal
        return notParallel.map(fn(np) -> {
            if(round_at(np.cross(normal).magnitude, 1) == 0.0) // sum up all parallel planes
                np + (np.normalized.dot(normal.normalized) * normal) // force normal to be in the same direction than np
            else
                np
        })
    })

sync fn fasten_sum_unoriented_vectors(unorientedVectors: Vec3[], reference: Vec3) -> Vec3 =
    unorientedVectors.reduce(vec3(0.0, 0.0, 0.0), fn(vsum, v) -> {
        if(round_at(reference.normalized.dot(v.normalized), 1) != 0.0) // mostly in direction of reference, if any
            vsum + (v * (reference.normalized.dot(v.normalized)))
        else
            vsum + v
    })

use { RgbaColor, color } from ksp::console

const WHITE: RgbaColor = color(1.0, 1.0, 1.0, 1.0)
const BLACK: RgbaColor = color(0.0, 0.0, 0.0, 1.0)
const GREY: RgbaColor = color(0.5, 0.5, 0.5, 1.0)

const RED: RgbaColor = color(1.0, 0.0, 0.0, 1.0)
const GREEN: RgbaColor = color(0.0, 1.0, 0.0, 1.0)
const BLUE: RgbaColor = color(0.0, 0.0, 1.0, 1.0)

const YELLOW: RgbaColor = color(1.0, 1.0, 0.0, 1.0)
const MAGENTA: RgbaColor = color(1.0, 0.0, 1.0, 1.0)
const CYAN: RgbaColor = color(0.0, 1.0, 1.0, 1.0)

const CRIMSON: RgbaColor = color(0.5, 0.0, 0.0, 1.0)
const FOREST: RgbaColor = color(0.0, 0.5, 0.0, 1.0)
const ULTRAMARINE: RgbaColor = color(0.0, 0.0, 0.5, 1.0)

const OLIVE: RgbaColor = color(0.5, 0.5, 0.0, 1.0)
const PURPLE: RgbaColor = color(0.5, 0.0, 0.5, 1.0)
const TURQUOISE: RgbaColor = color(0.0, 0.5, 0.5, 1.0)

const ORANGE: RgbaColor = color(1.0, 0.5, 0.0, 1.0)
const FUCHSIA: RgbaColor = color(1.0, 0.0, 0.5, 1.0)
const CORAL: RgbaColor = color(1.0, 0.5, 0.5, 1.0)

const LIME: RgbaColor = color(0.5, 1.0, 0.0, 1.0)
const MINT: RgbaColor = color(0.0, 1.0, 0.5, 1.0)
const SPRING: RgbaColor = color(0.5, 1.0, 0.5, 1.0)

const INDIGO: RgbaColor = color(0.5, 0.0, 1.0, 1.0)
const ELECTRIC: RgbaColor = color(0.0, 0.5, 1.0, 1.0)
const LAVENDER: RgbaColor = color(0.5, 0.5, 1.0, 1.0)

const CITRUS: RgbaColor = color(1.0, 1.0, 0.5, 1.0)
const MAUVE: RgbaColor = color(1.0, 0.5, 1.0, 1.0)
const AQUA: RgbaColor = color(0.5, 1.0, 1.0, 1.0)

Now, it's time to take my medications : A great shirt with very long sleeves, buckled up in my back and some 'Always Happy Sleepy Candies'.

untoldwind commented 3 months ago

I think I got it now.

To simplify things a bit, I skip the ideal_exposure calculation and just choose an arbitrary vector in the control_frame of the vessel and one that is perpendicular to it. Task is: Rotate the vessel so that the first vector points to the sun and the second vector to the orbit normal.

use { Vessel } from ksp::vessel
use { CONSOLE, BLUE, RED, YELLOW, GREEN } from ksp::console
use { DEBUG } from ksp::debug
use { vec3, Vec3, look_dir_up, GlobalVector } from ksp::math
use { sleep, wait_until, wait_while } from ksp::game
use { find_body, Orbit } from ksp::orbit
use { control_steering } from std::control::steering

pub fn main_flight(vessel: Vessel) -> Result<Unit, string> = {
    CONSOLE.clear()

    const frame = vessel.main_body.celestial_frame
    const pointToSunCf = vec3(3,1,2).normalized // Anything will do
    const pointToNormal = get_perpendicular(pointToSunCf).normalized

    const sun = find_body("Kerbol")?

    DEBUG.clear_markers()
    DEBUG.add_vector(fn() -> vessel.global_position, fn() -> 20 * (sun.global_position - vessel.global_position).normalized, BLUE, "Sun", 2)
    DEBUG.add_vector(fn() -> vessel.global_position, fn() -> 20 * get_global_orbit_normal(vessel.orbit), RED, "norm", 2)
    DEBUG.add_vector(fn() -> vessel.global_position, fn() -> 20 * pointToSunCf.to_global(vessel.control_frame), YELLOW, "To the sun", 2)
    DEBUG.add_vector(fn() -> vessel.global_position, fn() -> 20 * pointToNormal.to_global(vessel.control_frame), GREEN, "To normal", 2)

    const sunDir = look_dir_up((sun.global_position - vessel.global_position).normalized.to_local(frame), get_global_orbit_normal(vessel.orbit).to_local(frame))
    const pointToSunDir = look_dir_up(pointToSunCf.to_global(vessel.control_frame).to_local(frame), pointToNormal.to_global(vessel.control_frame).to_local(frame))
    const rotation = sunDir * pointToSunDir.inverse

    const steering = control_steering(vessel)

    steering.set_direction(rotation * vessel.facing)

    CONSOLE.print_line("Waiting")
    wait_while(fn() -> pointToSunCf.to_global(vessel.control_frame) * (sun.global_position - vessel.global_position).normalized < 0.999 &&
          pointToNormal.to_global(vessel.control_frame) * get_global_orbit_normal(vessel.orbit) < 0.999) 

    CONSOLE.print_line("Done")

    sleep(10000) // Sleep do admire the vectors
}

sync fn get_perpendicular(v: Vec3) -> Vec3 = if(v.z != 0) vec3(v.z, v.z, -v.x - v.y) else vec3(-v.y-v.z, v.x, v.x)

sync fn get_global_orbit_normal(orbit: Orbit) -> GlobalVector = orbit.orbit_normal.to_global(orbit.reference_frame)

I guess the main difference is that I replaced:

const rotation = (starVectorDir * vessel.facing) - (pointToSunCfDir * vessel.facing)

with

const rotation = sunDir * pointToSunDir.inverse

Edit: Just checked the implementation. The - operator on Direction actually subtracts the Euler-Angles from each other. I think I took that over from kOS once, though I can not think of a use-case where this could be useful. Though I guess this explains your convergence problem, the smaller the angle-differences to closer the - gets to the correct rotation. Edit2: It was/is in kOS as well: https://github.com/KSP-KOS/KOS/blob/c6425acb28e26cb86ffe895febcbf9168278575f/src/kOS/Suffixed/Direction.cs#L191

lefouvert commented 3 months ago

THAT ! This code is exactly what I was aiming for. Not only the result (well oriented vessel) but the concise way.

«Here the sun, here the orbit normal, here my record with pair of perpendicular vectors Three line of some black magic rotation (The part I was lacking the most) Apply, check and good bye.»

About Edit 1 : I don't see more usecase than you. But as you have seen, concept and math behind rotation are hard for me, it's probably why I can't see a usecase, even if it would have been usefull. I try to work on it but it's resultless at the time. I hope one day get rewardings (intelectualy, obviously ^^) from this workout but I don't believe it will happen. But this don't discourage me to continue to try to understand :P

About Edit 2: I'm a cheater :) In KOs, I was able to smuggle without Direction considerations :) Joke away, I have done a lot of works on autowrited script in KOs (for exemple, my boot file not only seeked for the good mission definition, but also searched for dependancy between scripts, write all the stuff found in the ship volume (minified ;) ), auto deleting himself to gain space, and rewriting a new boot sequence (really short, this one) to be able to automaticly restart and found the good entrypoint in case of powerfailure. So not so much about directions. And when Direction consideration were a mandatory, I was able to workaround, feeding steering with heading (without roll) which essentialy implies trigonometry and geometry, way easier topics for me. And as KOs was way slower (and volume space was a constrain), I didn't even consider to write a proper park() function. I only aligned a random flank with the sun, put the vessel on normal+ axis and Ciao Bye. But the idea is grinding me since this times :)

Edit : Thank you. I don't have the pleasure to have found the solution by myself, but I'm relieved from the weight of this non-working piece of code, which is far more valuable. It was burying me.

lefouvert commented 3 months ago

version 0.5.6.3 (Ckan)

Since it's when I was testing and cleaning ideal_exposure(), I post my reports here. I think I get 2 bugs.


First one with arrays : I want to add an item to an array. This item is type of Part[], so I get a matrix Part[][] I was doing this :

// ko v0.5.6.3 issue#141
// ok v0.5.7.x

use { Part } from ksp::vessel

fn foobar() -> Unit = {
    let foo: Part[][] = <Part[]>[]
    foo.reduce(<Part[]>[], fn(clean, partArr) -> {
        if(clean.length == 0)
            return clean + partArr
        return clean + partArr.filter(fn(part) -> !clean.flat_map(fn(a) -> a).exists(fn(p) -> p.position == part.position)) // TODO prefer a part.id than a part.position when it will be available
    }) // ok
}

It compile without error, runtime is also without error, result is as expected, Champagne! To tidy up the code a bit, I tried this :

// ko v0.5.6.3 issue#141
// ok v0.5.7.x

use { Part } from ksp::vessel

fn foobar() -> Unit = {
    let foo: Part[][] = <Part[]>[]
    foo.reduce(<Part[]>[], deduplicate) // ko
}

sync fn deduplicate(clean: Part[][], partArr: Part[]) -> Part[][] = {
    if(clean.length == 0)
        clean + partArr // whine about not being able to add a Part[] to a Part[][]
    clean + partArr.filter(fn(part) -> !clean.flat_map(fn(a) -> a).exists(fn(p) -> p.position == part.position)) // TODO prefer a part.id than a part.position when it will be available
}

And I get this compilation error :

Rebooted in 00:00:13.0079384 ERROR: [_gitreport\arrayarraygrow.to2(14, 10)] IncompatibleTypes Cannot Add a Part[][] with a ksp::vessel::Part[] ERROR: [_gitreport\arrayarraygrow.to2(11, 1)] IncompatibleTypes Function 'deduplicate' returns Unit but should return Part[][]

I know it can add this item to this array since the first code work. So I think something goes wrong.


The second one is hard to describe. I have a ship which is unable to follow a direction, and I dont know why. As soon I use control_steering() on it, it gently spin endlessly. I tried an other ship with 2 control modules rotated 90° one from the other, everything was fine. I don't know if it come from the ship or from some piece of code, that's why I share this. I tried some basic code (while orbiting) onto which make it spin while others ships (at least 5) don't (they just look at the wanted direction) :

use { Vessel, AutopilotMode } from ksp::vessel
use { euler } from ksp::math
use { CONSOLE } from ksp::console
use { sleep } from ksp::game

use { control_steering } from std::control::steering

/// Entry Point
pub fn main_flight(vessel: Vessel) -> Result<Unit, string> = {
    CONSOLE.clear()

    CONSOLE.print_line("stabilisation")
    vessel.release_control()
    vessel.autopilot.mode = AutopilotMode.StabilityAssist
    sleep(5)

    CONSOLE.print_line("applying rotation")
    const wheel = control_steering(vessel)
    wheel.set_direction(euler(17, 124, 53))
    sleep(30) // ok

    CONSOLE.print_line("release")
    wheel.release()
    vessel.autopilot.mode = AutopilotMode.StabilityAssist
    sleep(5) // beyblade go to bed.

    CONSOLE.print_line("end")
    return Ok({})
}

Here the ship : (please don't mind the suspicious shape, it was on purpose because it make me laught, but it wasn't expected it become public. Bad luck, it's the one causing me troubles.) Remainder : "[USER]\AppData\LocalLow\Intercept Games\Kerbal Space Program 2\Saves\SinglePlayer[SAVE NAME]\Workspaces" GrappeLauncher_Mini.json

Any hints ring a bell ?

untoldwind commented 3 months ago

For the deduplicate part. Writing it like this seems to be ok:

sync fn deduplicate(clean: Part[][], partArr: Part[]) -> Part[][] = {
    if(clean.length == 0)
        clean + [partArr] // whine about not being able to add a Part[] to a Part[][]
    else
        clean + [partArr.filter(fn(part) -> !clean.flat_map(fn(a) -> a).exists(fn(p) -> p.position == part.position))] // TODO prefer a part.id than a part.position when it will be available
}

... but I still have to figure out why the extra [ and ] are necessary. For some reason the compiler does not recognize Part[] as a valid element type of Part[][] in this situation (while in the lambda-case it does).

As for the grappe launcher: I guess there is indeed to some wired transformation error, but first I have to get that thing into a stable orbit for proper testing (... completely neglected any kind of optimization of my launch scripts so far).

lefouvert commented 3 months ago

So the '+' operator on arrays have 2 purpose ? Add item or concatanate ? I assume it work this way :

Grappe Launcher... Ah ! I forgot this thing is barely maneuverable and became crazy as soon as we go out of it's very small comfort zone. Manually, it's extremely risky to point out of the surface prograde green circle, and full thrust past 260m/s is to powerfull : it will flip since the nose is lightweight and have big drag. If you struggle to much, last version of my git code should be able to do the work, you just have to run the boot module. (This code made me lazy in Launcher conception and stability consideration x) )

untoldwind commented 3 months ago

Yes, the + can do both (or is supposed to):

untoldwind commented 3 months ago

One thing I noticed with the grappe launcher that - on the launchpad - north is to the right on the navball instead of down as for "regular" rockets, so there is some 90-degree twist. In there steering controller there is an ad-hoc

const VESSEL_ROTATION_INVERSE : Direction = euler(90, 0, 0)

that is mostly a remnant from KSP1 ... I guess that does not match for this particular rocket for some reason.

When I rotate the RC-001S module by 90 degrees (so that north is down on start) the steering works as intended. My guess is that when using the control_frame one also has to take the rotation of the control-module part into account ... but this requires deeper debugging.

lefouvert commented 3 months ago

Hum... Interesting. As it's one of my first build in sandbox mode, this vessel have the advantage (and disavantage) to suffer form all my ignorance at the time, some old reflex from KSP 1, and as it's a combination of multiple body, many command module, not made at the same time the ship was, not oriented the same way. In short, it's a mess. a functionnal mess, but still. (first launch in KSP2, I was disoriented because I used to set my pitch to east using «down» in KSP1 with default command module positionning and it's not the case anymore in KSP2) It's a disavantage because it lead to some complications. It's an advantage because it's make of this ship a good benchmark.

I would think than the potential difference between vessel.control_frame and vessel.body_frame could be the cause, but potentialy, it's deeper.

lefouvert commented 3 months ago

After some testings, I didn't found valuables knowleges...

I found you can rotate direction with a funny way vessel.global_facing.to_local(vessel.control_frame)).to_global(vessel.body_frame).to_local(vessel.main_body.celestial_frame)), give your the rotation beetween facing.vector and foward from the control frame

I found the assertion std::control::steering

// In vessel.control_frame these are constants
const VESSEL_ROTATION_INVERSE : Direction = euler(90, 0, 0)
const VESSEL_FORWARD : Vec3 = vec3(0, 1, 0)
const VESSEL_TOP : Vec3 = vec3(0, 0, -1)
const VESSEL_STARBOARD : Vec3 = vec3(1, 0, 0)

is true even in KSP2

I tried many other things, most meaning less, but one thing remains constant. Each time I try to rotate a vessel with a command module not in default orientation, control_steering(vessel) spin it as a spinning top. An easy way to observe it is to build a simple orbitable vessel, with 2 control modules, one in default orientation, the other one rotated 90° on the vertical axis. When orbited, if the first one is in control, everything is fine, when it's the second one (a nice button in part command allow to do it in flight), it's not the rotation is false (I would like, It would be easier), it's just spin spiiiin spiiiiIIIIiiiiN (but softly, thx to the pid)!

Edit : Isn't it the famous «gimbal lock» with could happen with rotation when done with rotation matrix ? I don't have enough knowlege to be able to identify it, I only know it exists. Because when looking at the Pitch Yaw Roll controls in the navball, It's not it try to spin it, it's just it don't give anymore steering modifications, as if it don't know what to do (And as it already have initiated the rotation, the spin continue). I remember the rotation matrix gimbal lock is about «And now, we don't know which axe to rotate». Can it be related ?

untoldwind commented 3 months ago

After a lot of digging: The angular_velocity seems to be in pitch-roll-yaw format (as seems to be the moment of interia and control_torque). That is were this VESSEL_ROTATION_INVERSE comes. So this here: https://github.com/untoldwind/KontrolSystem2/blob/8437adeb5bad4c845ddc6999af4069c619d47e6c/KSP2Runtime/to2/std/control/steering.to2#L103 should actually be

        const angular_velocity = self.vessel.global_angular_velocity.relative_to(mcframe).to_local(frame)
        self.omega = vec3(-angular_velocity.x, -angular_velocity.z, -angular_velocity.y)

Which actually might be the root cause (i.e. internally everything is done in pitch-roll-yaw while the SteeringController does its calculations in pitch-yaw-roll) ... ah, the fun of Euler angels

lefouvert commented 3 months ago

I've tried to update the line. It might be a part, if not the root of the issue, but at the time, this line doesn't solve my issue (at the time, it make it worst, since even the command module in default build mode is not able to properly steer/roll anymore x) ) I guess it's because the mixed convention is spread all along the code, and should be updated everywhere. But thank you for you time, I'll dig in this direction probably this evening. For now, I have to get a machine gun, I have some angels to take down. (yup, I get it, it's a typo)

untoldwind commented 3 months ago

Yes, well: How many Euler angels can dance on the tip of my nose ... or something like that ;)

Anyhow: Here is an updated version that uses way less magic-vectors and works for regular vessels and those with a twisted command module (at least as far as I could test it): https://github.com/untoldwind/KontrolSystem2/blob/prerelease/KSP2Runtime/to2/std/control/steering.to2

Actually all the angle calculation was not really the problem, the actual problem was this line: https://github.com/untoldwind/KontrolSystem2/blob/8437adeb5bad4c845ddc6999af4069c619d47e6c/KSP2Runtime/to2/std/control/steering.to2#L171

For some reason self.max_yaw_omega became negative, which completely messed up the PID loop, so a simple abs(...) fixes it.

Though I am still investigating why that happened, neither torque nor moment of inertia should be negative

Edit: Strike that ... of course it can happen if either one is transforms to a different frame of reference.

lefouvert commented 3 months ago

Dude. It works. On all my ships. I'm happy. Really. Now, I have to understand, but it will be easier (or less hard, maybe ^^) with a resiliant example ^^

github-actions[bot] commented 1 month ago

This issue is stale because it has been open for 60 days with no activity.

github-actions[bot] commented 3 weeks ago

This issue was closed because it has been inactive for 14 days since being marked as stale.